diff options
88 files changed, 13508 insertions, 6270 deletions
diff --git a/app/Module/StatisticsChartModule.php b/app/Module/StatisticsChartModule.php index d84f6e154c..ce7622c99c 100644 --- a/app/Module/StatisticsChartModule.php +++ b/app/Module/StatisticsChartModule.php @@ -275,14 +275,14 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->statsBirthQuery(false, false); + $rows = $stats->statsBirthQuery(); foreach ($rows as $row) { $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata); } break; case self::Z_AXIS_SEX: $z_axis = $this->axisSexes(); - $rows = $stats->statsBirthQuery(false, true); + $rows = $stats->statsBirthQuery(true); foreach ($rows as $row) { $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata); } @@ -292,7 +292,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisYears($boundaries_csv); $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsBirthQuery(false, false, $prev_boundary, $boundary); + $rows = $stats->statsBirthQuery(false, $prev_boundary, $boundary); foreach ($rows as $row) { $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata); } @@ -324,14 +324,14 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->statsDeathQuery(false, false); + $rows = $stats->statsDeathQuery(); foreach ($rows as $row) { $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata); } break; case self::Z_AXIS_SEX: $z_axis = $this->axisSexes(); - $rows = $stats->statsDeathQuery(false, true); + $rows = $stats->statsDeathQuery(true); foreach ($rows as $row) { $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata); } @@ -341,7 +341,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisYears($boundaries_csv); $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsDeathQuery(false, false, $prev_boundary, $boundary); + $rows = $stats->statsDeathQuery(false, $prev_boundary, $boundary); foreach ($rows as $row) { $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata); } @@ -373,7 +373,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->statsMarrQuery(false, false); + $rows = $stats->statsMarrQuery(); foreach ($rows as $row) { $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata); } @@ -383,7 +383,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisYears($boundaries_csv); $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsMarrQuery(false, false, $prev_boundary, $boundary); + $rows = $stats->statsMarrQuery(false, $prev_boundary, $boundary); foreach ($rows as $row) { $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata); } @@ -415,7 +415,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->monthFirstChildQuery(false); + $rows = $stats->monthFirstChildQuery(); foreach ($rows as $row) { $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata); } @@ -432,7 +432,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisYears($boundaries_csv); $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsMarrQuery(false, false, $prev_boundary, $boundary); + $rows = $stats->statsMarrQuery(false, $prev_boundary, $boundary); foreach ($rows as $row) { $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata); } @@ -464,7 +464,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->statsMarrQuery(false, true); + $rows = $stats->statsMarrQuery(true); $indi = []; $fam = []; foreach ($rows as $row) { @@ -482,7 +482,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $indi = []; $fam = []; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsMarrQuery(false, true, $prev_boundary, $boundary); + $rows = $stats->statsMarrQuery(true, $prev_boundary, $boundary); foreach ($rows as $row) { if (!in_array($row->indi, $indi) && !in_array($row->fams, $fam)) { $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata); @@ -519,7 +519,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->statsAgeQuery(false, 'DEAT'); + $rows = $stats->statsAgeQuery('DEAT'); foreach ($rows as $row) { foreach ($row as $age) { $years = (int) ($age / self::DAYS_IN_YEAR); @@ -530,7 +530,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa case self::Z_AXIS_SEX: $z_axis = $this->axisSexes(); foreach (array_keys($z_axis) as $sex) { - $rows = $stats->statsAgeQuery(false, 'DEAT', $sex); + $rows = $stats->statsAgeQuery('DEAT', $sex); foreach ($rows as $row) { foreach ($row as $age) { $years = (int) ($age / self::DAYS_IN_YEAR); @@ -544,7 +544,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisYears($boundaries_csv); $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsAgeQuery(false, 'DEAT', 'BOTH', $prev_boundary, $boundary); + $rows = $stats->statsAgeQuery('DEAT', 'BOTH', $prev_boundary, $boundary); foreach ($rows as $row) { foreach ($row as $age) { $years = (int) ($age / self::DAYS_IN_YEAR); @@ -583,7 +583,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisAll(); // The stats query doesn't have an "all" function, so query M/F/U separately foreach (['M', 'F', 'U'] as $sex) { - $rows = $stats->statsMarrAgeQuery(false, $sex); + $rows = $stats->statsMarrAgeQuery($sex); foreach ($rows as $row) { $years = (int) ($row->age / self::DAYS_IN_YEAR); $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata); @@ -593,7 +593,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa case self::Z_AXIS_SEX: $z_axis = $this->axisSexes(); foreach (array_keys($z_axis) as $sex) { - $rows = $stats->statsMarrAgeQuery(false, $sex); + $rows = $stats->statsMarrAgeQuery($sex); foreach ($rows as $row) { $years = (int) ($row->age / self::DAYS_IN_YEAR); $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata); @@ -607,7 +607,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa foreach (['M', 'F', 'U'] as $sex) { $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsMarrAgeQuery(false, $sex, $prev_boundary, $boundary); + $rows = $stats->statsMarrAgeQuery($sex, $prev_boundary, $boundary); foreach ($rows as $row) { $years = (int) ($row->age / self::DAYS_IN_YEAR); $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata); @@ -644,7 +644,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisAll(); // The stats query doesn't have an "all" function, so query M/F/U separately foreach (['M', 'F', 'U'] as $sex) { - $rows = $stats->statsMarrAgeQuery(false, $sex); + $rows = $stats->statsMarrAgeQuery($sex); $indi = []; foreach ($rows as $row) { if (!in_array($row->d_gid, $indi)) { @@ -658,7 +658,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa case self::Z_AXIS_SEX: $z_axis = $this->axisSexes(); foreach (array_keys($z_axis) as $sex) { - $rows = $stats->statsMarrAgeQuery(false, $sex); + $rows = $stats->statsMarrAgeQuery($sex); $indi = []; foreach ($rows as $row) { if (!in_array($row->d_gid, $indi)) { @@ -677,7 +677,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $prev_boundary = 0; $indi = []; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsMarrAgeQuery(false, $sex, $prev_boundary, $boundary); + $rows = $stats->statsMarrAgeQuery($sex, $prev_boundary, $boundary); foreach ($rows as $row) { if (!in_array($row->d_gid, $indi)) { $years = (int) ($row->age / self::DAYS_IN_YEAR); @@ -714,7 +714,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa switch ($z_axis_type) { case self::Z_AXIS_ALL: $z_axis = $this->axisAll(); - $rows = $stats->statsChildrenQuery(false); + $rows = $stats->statsChildrenQuery(); foreach ($rows as $row) { $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata); } @@ -724,7 +724,7 @@ class StatisticsChartModule extends AbstractModule implements ModuleChartInterfa $z_axis = $this->axisYears($boundaries_csv); $prev_boundary = 0; foreach (array_keys($z_axis) as $boundary) { - $rows = $stats->statsChildrenQuery(false, 'BOTH', $prev_boundary, $boundary); + $rows = $stats->statsChildrenQuery('BOTH', $prev_boundary, $boundary); foreach ($rows as $row) { $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata); } diff --git a/app/Statistics/AbstractGoogle.php b/app/Statistics/AbstractGoogle.php new file mode 100644 index 0000000000..8710551d71 --- /dev/null +++ b/app/Statistics/AbstractGoogle.php @@ -0,0 +1,91 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics; + +use Fisharebest\Webtrees\Statistics\Helper\Sql; + +/** + * Base class for all google charts. + * + * @deprecated The pie chart API is outdated and should be replaced + * by the newer version https://developers.google.com/chart/ or + * an open source one like chart.js + * + * @see https://developers.google.com/chart/image/docs/gallery/pie_charts + */ +abstract class AbstractGoogle +{ + // Used in Google charts + public const GOOGLE_CHART_ENCODING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'; + + /** + * Convert numbers to Google's custom encoding. + * + * @link http://bendodson.com/news/google-extended-encoding-made-easy + * + * @param int[] $a + * + * @return string + */ + protected function arrayToExtendedEncoding(array $a): string + { + $xencoding = self::GOOGLE_CHART_ENCODING; + $encoding = ''; + + foreach ($a as $value) { + if ($value < 0) { + $value = 0; + } + + $first = intdiv($value, 64); + $second = $value % 64; + $encoding .= $xencoding[$first] . $xencoding[$second]; + } + + return $encoding; + } + + /** + * Returns the three-dimensional pie chart url. + * + * @param string $data + * @param string $size + * @param array $colors + * @param string $labels + * + * @return string + */ + protected function getPieChartUrl(string $data, string $size, array $colors, string $labels): string + { + return 'https://chart.googleapis.com/chart?cht=p3&chd=e:' . $data + . '&chs=' . $size . '&chco=' . implode(',', $colors) . '&chf=bg,s,ffffff00&chl=' + . $labels; + } + + /** + * Run an SQL query and cache the result. + * + * @param string $sql + * + * @return \stdClass[] + */ + protected function runSql(string $sql): array + { + return Sql::runSql($sql); + } +} diff --git a/app/Statistics/Google/ChartAge.php b/app/Statistics/Google/ChartAge.php new file mode 100644 index 0000000000..0ae0939b17 --- /dev/null +++ b/app/Statistics/Google/ChartAge.php @@ -0,0 +1,170 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Tree; + +/** + * + */ +class ChartAge extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @return \stdClass[] + */ + private function queryRecords(): array + { + return $this->runSql( + 'SELECT' + . ' 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->id() . ' 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' + ); + } + + /** + * General query on ages. + * + * @param string $size + * + * @return string + */ + public function chartAge(string $size = '230x250'): string + { + $sizes = explode('x', $size); + $rows = $this->queryRecords(); + + if (empty($rows)) { + return ''; + } + + $chxl = '0:|'; + $countsm = ''; + $countsf = ''; + $countsa = ''; + $out = []; + + foreach ($rows as $values) { + $out[(int) $values->century][$values->sex] = $values->age; + } + + foreach ($out as $century => $values) { + if ($sizes[0] < 980) { + $sizes[0] += 50; + } + $chxl .= $this->centuryHelper->centuryName($century) . '|'; + + $female_age = $values['F'] ?? 0; + $male_age = $values['M'] ?? 0; + $average_age = $female_age + $male_age; + + if ($female_age > 0 && $male_age > 0) { + $average_age /= 2.0; + } + + $countsf .= $female_age . ','; + $countsm .= $male_age . ','; + $countsa .= $average_age . ','; + } + + $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 = []; + + while ($offset = strpos($title, ' ', $offset + 1)) { + $counter[] = $offset; + } + + $half = intdiv(\count($counter), 2); + $chtt = substr_replace($title, '|', $counter[$half], 1); + } + + $chart_url = 'https://chart.googleapis.com/chart?cht=bvg&chs=' . $sizes[0] . 'x' . $sizes[1] + . '&chm=D,FF0000,2,0,3,1|N*f1*,000000,0,-1,11,1|N*f1*,000000,1,-1,11,1&chf=bg,s,ffffff00|c,s,ffffff00&chtt=' + . rawurlencode($chtt) . '&chd=' . $chd . '&chco=0000FF,FFA0CB,FF0000&chbh=20,3&chxt=x,x,y,y&chxl=' + . rawurlencode($chxl) . '&chdl=' + . rawurlencode(I18N::translate('Males') . '|' . I18N::translate('Females') . '|' . I18N::translate('Average age at death')); + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Average age related to death century'), + 'chart_url' => $chart_url, + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartBirth.php b/app/Statistics/Google/ChartBirth.php new file mode 100644 index 0000000000..7dcf78ddb7 --- /dev/null +++ b/app/Statistics/Google/ChartBirth.php @@ -0,0 +1,126 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * + */ +class ChartBirth extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @return \stdClass[] + */ + private function queryRecords(): array + { + $query = DB::table('dates') + ->selectRaw('FLOOR(d_year / 100 + 1) AS century') + ->selectRaw('COUNT(*) AS total') + ->where('d_file', '=', $this->tree->id()) + ->where('d_year', '<>', 0) + ->where('d_fact', '=', 'BIRT') + ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) + ->groupBy(['century']) + ->orderBy('century'); + + return $query->get()->all(); + } + + /** + * Create a chart of birth places. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartBirth(string $size = null, string $color_from = null, string $color_to = null): string + { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + + $sizes = explode('x', $size); + $tot = 0; + $rows = $this->queryRecords(); + + foreach ($rows as $values) { + $tot += $values->total; + } + + // Beware divide by zero + if ($tot === 0) { + return ''; + } + + $centuries = ''; + $counts = []; + foreach ($rows as $values) { + $counts[] = intdiv(100 * $values->total, $tot); + $centuries .= $this->centuryHelper->centuryName($values->century) . ' - ' . I18N::number($values->total) . '|'; + } + + $chd = $this->arrayToExtendedEncoding($counts); + $chl = rawurlencode(substr($centuries, 0, -1)); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Births by century'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartChildren.php b/app/Statistics/Google/ChartChildren.php new file mode 100644 index 0000000000..cc0f60a8f1 --- /dev/null +++ b/app/Statistics/Google/ChartChildren.php @@ -0,0 +1,144 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\JoinClause; + +/** + * + */ +class ChartChildren extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @return \stdClass[] + */ + private function queryRecords(): array + { + $query = DB::table('families') + ->selectRaw('ROUND(AVG(f_numchil),2) AS num') + ->selectRaw('FLOOR(d_year / 100 + 1) AS century') + ->join('dates', function (JoinClause $join) { + $join->on('d_file', '=', 'f_file') + ->on('d_gid', '=', 'f_id'); + }) + ->where('f_file', '=', $this->tree->id()) + ->where('d_julianday1', '<>', 0) + ->where('d_fact', '=', 'MARR') + ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) + ->groupBy(['century']) + ->orderBy('century'); + + return $query->get()->all(); + } + + /** + * General query on familes/children. + * + * @param string $size + * + * @return string + */ + public function chartChildren(string $size = '220x200'): string + { + $sizes = explode('x', $size); + $max = 0; + $rows = $this->queryRecords(); + + if (empty($rows)) { + return ''; + } + + foreach ($rows as $values) { + $values->num = (int) $values->num; + if ($max < $values->num) { + $max = $values->num; + } + } + + $chm = ''; + $chxl = '0:|'; + $i = 0; + $counts = []; + + foreach ($rows as $values) { + $chxl .= $this->centuryHelper->centuryName((int) $values->century) . '|'; + if ($max <= 5) { + $counts[] = (int) ($values->num * 819.2 - 1); + } elseif ($max <= 10) { + $counts[] = (int) ($values->num * 409.6); + } else { + $counts[] = (int) ($values->num * 204.8); + } + $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') . '|'; + } + + $chart_url = 'https://chart.googleapis.com/chart?cht=bvg&chs=' . $sizes[0] . 'x' . $sizes[1] + . '&chf=bg,s,ffffff00|c,s,ffffff00&chm=D,FF0000,0,0,3,1|' . $chm + . '&chd=e:' . $chd . '&chco=0000FF&chbh=30,3&chxt=x,x,y,y&chxl=' + . rawurlencode($chxl); + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Average number of children per family'), + 'chart_url' => $chart_url, + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartCommonGiven.php b/app/Statistics/Google/ChartCommonGiven.php new file mode 100644 index 0000000000..d7a4a953b1 --- /dev/null +++ b/app/Statistics/Google/ChartCommonGiven.php @@ -0,0 +1,96 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; + +/** + * + */ +class ChartCommonGiven extends AbstractGoogle +{ + /** + * Create a chart of common given names. + * + * @param int $tot_indi The total number of individuals + * @param array $given The list of common given names + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartCommonGiven( + int $tot_indi, + array $given, + string $size = null, + string $color_from = null, + string $color_to = null + ) : string { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + $sizes = explode('x', $size); + + if (empty($given)) { + return ''; + } + + $tot = 0; + foreach ($given as $count) { + $tot += $count; + } + + $chd = ''; + $chl = []; + + foreach ($given as $givn => $count) { + if ($tot === 0) { + $per = 0; + } else { + $per = intdiv(100 * $count, $tot_indi); + } + $chd .= $this->arrayToExtendedEncoding([$per]); + $chl[] = $givn . ' - ' . I18N::number($count); + } + + $per = intdiv(100 * ($tot_indi - $tot), $tot_indi); + $chd .= $this->arrayToExtendedEncoding([$per]); + $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); + + $chart_title = implode(I18N::$list_separator, $chl); + $chl = rawurlencode(implode('|', $chl)); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => $chart_title, + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartCommonSurname.php b/app/Statistics/Google/ChartCommonSurname.php new file mode 100644 index 0000000000..e6ff0422ee --- /dev/null +++ b/app/Statistics/Google/ChartCommonSurname.php @@ -0,0 +1,142 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; + +/** + * + */ +class ChartCommonSurname extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Create a chart of common surnames. + * + * @param int $tot_indi The total number of individuals + * @param array $all_surnames The list of common surnames + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartCommonSurnames( + int $tot_indi, + array $all_surnames, + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + $sizes = explode('x', $size); + + if (empty($all_surnames)) { + return ''; + } + + $surname_tradition = $this->tree->getPreference('SURNAME_TRADITION'); + + $tot = 0; + + foreach ($all_surnames as $surn => $surnames) { + $tot += array_sum($surnames); + } + + $chd = ''; + $chl = []; + + /** @var array $surns */ + foreach ($all_surnames as $surns) { + $count_per = 0; + $max_name = 0; + $top_name = ''; + + foreach ($surns as $spfxsurn => $count) { + $per = $count; + $count_per += $per; + + // select most common surname from all variants + if ($per > $max_name) { + $max_name = $per; + $top_name = $spfxsurn; + } + } + + if ($surname_tradition === 'polish') { + // Most common surname should be in male variant (Kowalski, not Kowalska) + $top_name = preg_replace([ + '/ska$/', + '/cka$/', + '/dzka$/', + '/żka$/', + ], [ + 'ski', + 'cki', + 'dzki', + 'żki', + ], $top_name); + } + + $per = intdiv(100 * $count_per, $tot_indi); + $chd .= $this->arrayToExtendedEncoding([$per]); + $chl[] = $top_name . ' - ' . I18N::number($count_per); + } + + $per = intdiv(100 * ($tot_indi - $tot), $tot_indi); + $chd .= $this->arrayToExtendedEncoding([$per]); + $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); + + $chart_title = implode(I18N::$list_separator, $chl); + $chl = rawurlencode(implode('|', $chl)); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => $chart_title, + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartDeath.php b/app/Statistics/Google/ChartDeath.php new file mode 100644 index 0000000000..acbacf79d2 --- /dev/null +++ b/app/Statistics/Google/ChartDeath.php @@ -0,0 +1,127 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * + */ +class ChartDeath extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @return \stdClass[] + */ + private function queryRecords(): array + { + $query = DB::table('dates') + ->selectRaw('FLOOR(d_year / 100 + 1) AS century') + ->selectRaw('COUNT(*) AS total') + ->where('d_file', '=', $this->tree->id()) + ->where('d_year', '<>', 0) + ->where('d_fact', '=', 'DEAT') + ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) + ->groupBy(['century']) + ->orderBy('century'); + + return $query->get()->all(); + } + + /** + * Create a chart of death places. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartDeath(string $size = null, string $color_from = null, string $color_to = null): string + { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + + $sizes = explode('x', $size); + $tot = 0; + $rows = $this->queryRecords(); + + foreach ($rows as $values) { + $values->total = (int) $values->total; + $tot += $values->total; + } + + // Beware divide by zero + if ($tot === 0) { + return ''; + } + + $centuries = ''; + $counts = []; + foreach ($rows as $values) { + $counts[] = intdiv(100 * $values->total, $tot); + $centuries .= $this->centuryHelper->centuryName((int) $values->century) . ' - ' . I18N::number($values->total) . '|'; + } + + $chd = $this->arrayToExtendedEncoding($counts); + $chl = rawurlencode(substr($centuries, 0, -1)); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Deaths by century'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartDistribution.php b/app/Statistics/Google/ChartDistribution.php new file mode 100644 index 0000000000..5664f09972 --- /dev/null +++ b/app/Statistics/Google/ChartDistribution.php @@ -0,0 +1,236 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\Database; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Helper\Country; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Repository\PlaceRepository; +use Fisharebest\Webtrees\Statistics\Repository\IndividualRepository; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; + +/** + * Create a chart showing where events occurred. + */ +class ChartDistribution extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Country + */ + private $countryHelper; + + /** + * @var IndividualRepository + */ + private $individualRepository; + + /** + * @var PlaceRepository + */ + private $placeRepository; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->countryHelper = new Country(); + $this->individualRepository = new IndividualRepository($tree); + $this->placeRepository = new PlaceRepository($tree); + } + + /** + * Create a chart showing where events occurred. + * + * @param int $tot_pl The total number of places + * @param string $chart_shows + * @param string $chart_type + * @param string $surname + * + * @return string + */ + public function chartDistribution( + int $tot_pl, + string $chart_shows = 'world', + string $chart_type = '', + string $surname = '' + ): string { + $chart_color1 = Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = Theme::theme()->parameter('distribution-chart-high-values'); + $chart_color3 = Theme::theme()->parameter('distribution-chart-low-values'); + $map_x = Theme::theme()->parameter('distribution-chart-x'); + $map_y = Theme::theme()->parameter('distribution-chart-y'); + + if ($tot_pl === 0) { + return ''; + } + + $countries = $this->countryHelper->getAllCountries(); + + // Get the country names for each language + $country_to_iso3166 = []; + foreach (I18N::activeLocales() as $locale) { + I18N::init($locale->languageTag()); + + foreach ($this->countryHelper->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->individualRepository->getCommonSurname(); + } + $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname; + // Count how many people are events in each country + $surn_countries = []; + + $rows = Database::prepare( + 'SELECT i_gedcom' . ' FROM `##individuals`' . ' JOIN `##name` ON n_id = i_id AND n_file = i_file' . ' WHERE n_file = :tree_id' . ' AND n_surn COLLATE :collate = :surname' + )->execute([ + 'tree_id' => $this->tree->id(), + 'collate' => I18N::collation(), + 'surname' => $surname, + ])->fetchAll(); + + foreach ($rows as $row) { + if (preg_match_all('/^2 PLAC (?:.*, *)*(.*)/m', $row->i_gedcom, $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 = []; + $b_countries = $this->placeRepository->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 = []; + $d_countries = $this->placeRepository->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 = []; + $m_countries = $this->placeRepository->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 = []; + $a_countries = $this->placeRepository->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=' . $chart_color1 . ',' . $chart_color3 . ',' . $chart_color2; // country colours + $chart_url .= '&chf=bg,s,ECF5FF'; // sea colour + $chart_url .= '&chs=' . $map_x . 'x' . $map_y; + $chart_url .= '&chld=' . implode('', array_keys($surn_countries)) . '&chd=s:'; + + foreach ($surn_countries as $count) { + $chart_url .= substr(self::GOOGLE_CHART_ENCODING, (int) ($count / max($surn_countries) * 61), 1); + } + + return view( + 'statistics/other/chart-distribution', + [ + 'chart_title' => $chart_title, + 'chart_url' => $chart_url, + 'chart_color1' => $chart_color1, + 'chart_color2' => $chart_color2, + 'chart_color3' => $chart_color3, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartDivorce.php b/app/Statistics/Google/ChartDivorce.php new file mode 100644 index 0000000000..d651450f80 --- /dev/null +++ b/app/Statistics/Google/ChartDivorce.php @@ -0,0 +1,126 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * + */ +class ChartDivorce extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @return \stdClass[] + */ + private function queryRecords(): array + { + $query = DB::table('dates') + ->selectRaw('FLOOR(d_year / 100 + 1) AS century') + ->selectRaw('COUNT(*) AS total') + ->where('d_file', '=', $this->tree->id()) + ->where('d_year', '<>', 0) + ->where('d_fact', '=', 'DIV') + ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) + ->groupBy(['century']) + ->orderBy('century'); + + return $query->get()->all(); + } + + /** + * General query on divorces. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartDivorce(string $size = null, string $color_from = null, string $color_to = null): string + { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + + $sizes = explode('x', $size); + $tot = 0; + $rows = $this->queryRecords(); + + foreach ($rows as $values) { + $values->total = (int) $values->total; + $tot += $values->total; + } + // Beware divide by zero + if ($tot === 0) { + return ''; + } + $centuries = ''; + $counts = []; + + foreach ($rows as $values) { + $counts[] = intdiv(100 * $values->total, $tot); + $centuries .= $this->centuryHelper->centuryName((int) $values->century) . ' - ' . I18N::number($values->total) . '|'; + } + + $chd = $this->arrayToExtendedEncoding($counts); + $chl = substr($centuries, 0, -1); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Divorces by century'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartFamilyLargest.php b/app/Statistics/Google/ChartFamilyLargest.php new file mode 100644 index 0000000000..08ba4dfdb5 --- /dev/null +++ b/app/Statistics/Google/ChartFamilyLargest.php @@ -0,0 +1,131 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * + */ +class ChartFamilyLargest extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Returns the related database records. + * + * @param int $total + * + * @return \stdClass[] + */ + private function queryRecords(int $total): array + { + $query = DB::table('families') + ->select(['f_numchil AS tot', 'f_id AS id']) + ->where('f_file', '=', $this->tree->id()) + ->orderBy('tot', 'desc') + ->limit($total); + + return $query->get()->all(); + } + + /** + * Create a chart of the largest families. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * @param int $total + * + * @return string + */ + public function chartLargestFamilies( + string $size = null, + string $color_from = null, + string $color_to = null, + int $total = 10 + ): string { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-large-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? $chart_x . 'x' . $chart_y; + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + $sizes = explode('x', $size); + $rows = $this->queryRecords($total); + + if (!isset($rows[0])) { + return ''; + } + + $tot = 0; + foreach ($rows as $row) { + $tot += $row->tot; + } + + $chd = ''; + $chl = []; + + foreach ($rows as $row) { + $family = Family::getInstance($row->id, $this->tree); + + if ($family && $family->canShow()) { + if ($tot === 0) { + $per = 0; + } else { + $per = intdiv(100 * $row->tot, $tot); + } + + $chd .= $this->arrayToExtendedEncoding([$per]); + $chl[] = htmlspecialchars_decode(strip_tags($family->getFullName())) . ' - ' . I18N::number($row->tot); + } + } + + $chl = rawurlencode(implode('|', $chl)); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Largest families'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartFamilyWithSources.php b/app/Statistics/Google/ChartFamilyWithSources.php new file mode 100644 index 0000000000..ec966fd4dc --- /dev/null +++ b/app/Statistics/Google/ChartFamilyWithSources.php @@ -0,0 +1,77 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; + +/** + * + */ +class ChartFamilyWithSources extends AbstractGoogle +{ + /** + * Create a chart of individuals with/without sources. + * + * @param int $tot_fam The total number of families + * @param int $tot_fam_source The total number of families with sources + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartFamsWithSources( + int $tot_fam, + int $tot_fam_source, + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + + $sizes = explode('x', $size); + + if ($tot_fam === 0) { + return ''; + } + + $tot_sfam_per = $tot_fam_source / $tot_fam; + $with = (int) (100 * $tot_sfam_per); + $chd = $this->arrayToExtendedEncoding([100 - $with, $with]); + $chl = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sfam_per, 1) . '|' . I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sfam_per, 1); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Families with sources'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartIndividual.php b/app/Statistics/Google/ChartIndividual.php new file mode 100644 index 0000000000..fbf33d692f --- /dev/null +++ b/app/Statistics/Google/ChartIndividual.php @@ -0,0 +1,73 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; + +/** + * + */ +class ChartIndividual extends AbstractGoogle +{ + /** + * Create a chart showing individuals with/without sources. + * + * @param int $tot_indi The total number of individuals + * @param int $tot_indi_source The total number of individuals with sources + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartIndisWithSources( + int $tot_indi, int $tot_indi_source, string $size = null, string $color_from = null, string $color_to = null + ): string { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + + $sizes = explode('x', $size); + + if ($tot_indi === 0) { + return ''; + } + + $tot_sindi_per = $tot_indi_source / $tot_indi; + $with = (int) (100 * $tot_sindi_per); + $chd = $this->arrayToExtendedEncoding([100 - $with, $with]); + $chl = I18N::translate('Without sources') . ' - ' . I18N::percentage(1 - $tot_sindi_per, 1) . '|' . I18N::translate('With sources') . ' - ' . I18N::percentage($tot_sindi_per, 1); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Individuals with sources'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartMarriage.php b/app/Statistics/Google/ChartMarriage.php new file mode 100644 index 0000000000..a6ba2edfcb --- /dev/null +++ b/app/Statistics/Google/ChartMarriage.php @@ -0,0 +1,127 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * + */ +class ChartMarriage extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @return \stdClass[] + */ + private function queryRecords(): array + { + $query = DB::table('dates') + ->selectRaw('FLOOR(d_year / 100 + 1) AS century') + ->selectRaw('COUNT(*) AS total') + ->where('d_file', '=', $this->tree->id()) + ->where('d_year', '<>', 0) + ->where('d_fact', '=', 'MARR') + ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) + ->groupBy(['century']) + ->orderBy('century'); + + return $query->get()->all(); + } + + /** + * General query on marriages. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartMarriage(string $size = null, string $color_from = null, string $color_to = null): string + { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + + $sizes = explode('x', $size); + $tot = 0; + $rows = $this->queryRecords(); + + foreach ($rows as $values) { + $values->total = (int) $values->total; + $tot += (int) $values->total; + } + + // Beware divide by zero + if ($tot === 0) { + return ''; + } + + $centuries = ''; + $counts = []; + foreach ($rows as $values) { + $counts[] = intdiv(100 * $values->total, $tot); + $centuries .= $this->centuryHelper->centuryName((int) $values->century) . ' - ' . I18N::number($values->total) . '|'; + } + + $chd = $this->arrayToExtendedEncoding($counts); + $chl = substr($centuries, 0, -1); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('v by century'), + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartMarriageAge.php b/app/Statistics/Google/ChartMarriageAge.php new file mode 100644 index 0000000000..f6eead497c --- /dev/null +++ b/app/Statistics/Google/ChartMarriageAge.php @@ -0,0 +1,217 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Tree; + +/** + * + */ +class ChartMarriageAge extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @param string $sex + * + * @return \stdClass[] + */ + private function queryRecords(string $sex): array + { + // TODO + return $this->runSql( + 'SELECT ' + . ' 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->id()} 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->id()} 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' + ); + } + + /** + * General query on ages at marriage. + * + * @param string $size + * + * @return string + */ + public function chartMarriageAge(string $size = '200x250'): string + { + $sex = 'BOTH'; + $sizes = explode('x', $size); + $rows = $this->queryRecords($sex); + + if (empty($rows)) { + return ''; + } + + $max = 0; + + foreach ($rows as $values) { + $values->age = (int) $values->age; + if ($max < $values->age) { + $max = $values->age; + } + } + + $chxl = '0:|'; + $chmm = ''; + $chmf = ''; + $i = 0; + $countsm = ''; + $countsf = ''; + $countsa = ''; + $out = []; + + foreach ($rows as $values) { + $out[(int) $values->century][$values->sex] = $values->age; + } + + foreach ($out as $century => $values) { + if ($sizes[0] < 1000) { + $sizes[0] += 50; + } + $chxl .= $this->centuryHelper->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 = []; + + while ($offset = strpos(I18N::translate('Average age in century of marriage'), ' ', $offset + 1)) { + $counter[] = $offset; + } + + $half = intdiv(\count($counter), 2); + $chtt = substr_replace(I18N::translate('Average age in century of marriage'), '|', $counter[$half], 1); + } + + $chart_url = 'https://chart.googleapis.com/chart?cht=bvg&chs=' . $sizes[0] . 'x' . $sizes[1] + . '&chm=D,FF0000,2,0,3,1|' . $chmm . $chmf + . '&chf=bg,s,ffffff00|c,s,ffffff00&chtt=' . rawurlencode($chtt) + . '&chd=' . $chd . '&chco=0000FF,FFA0CB,FF0000&chbh=20,3&chxt=x,x,y,y&chxl=' + . rawurlencode($chxl) . '&chdl=' + . rawurlencode(I18N::translate('Males') . '|' . I18N::translate('Females') . '|' . I18N::translate('Average age')); + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Average age in century of marriage'), + 'chart_url' => $chart_url, + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartMedia.php b/app/Statistics/Google/ChartMedia.php new file mode 100644 index 0000000000..aeca532818 --- /dev/null +++ b/app/Statistics/Google/ChartMedia.php @@ -0,0 +1,88 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\GedcomTag; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Theme; + +/** + * + */ +class ChartMedia extends AbstractGoogle +{ + /** + * Create a chart of media types. + * + * @param int $tot The total number of media files + * @param array $media The list of media types to display + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartMedia( + int $tot, + array $media, + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + $chart_color1 = (string) Theme::theme()->parameter('distribution-chart-no-values'); + $chart_color2 = (string) Theme::theme()->parameter('distribution-chart-high-values'); + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_from = $color_from ?? $chart_color1; + $color_to = $color_to ?? $chart_color2; + $sizes = explode('x', $size); + + // 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 = []; + $mediaTypes = ''; + $chart_title = ''; + + foreach ($media as $type => $count) { + $mediaCounts[] = intdiv(100 * $count, $tot); + $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); + $colors = [$color_from, $color_to]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => $chart_title, + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartMortality.php b/app/Statistics/Google/ChartMortality.php new file mode 100644 index 0000000000..944e39b9ae --- /dev/null +++ b/app/Statistics/Google/ChartMortality.php @@ -0,0 +1,107 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Repository\IndividualRepository; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; + +/** + * + */ +class ChartMortality extends AbstractGoogle +{ + /** + * @var IndividualRepository + */ + private $individualRepository; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->individualRepository = new IndividualRepository($tree); + } + + /** + * Create a chart showing mortality. + * + * @param int $tot_l + * @param int $tot_d + * @param string|null $size + * @param string|null $color_living + * @param string|null $color_dead + * + * @return string + */ + public function chartMortality( + int $tot_l, + int $tot_d, + string $size = null, + string $color_living = null, + string $color_dead = null + ): string { + // Raw data - for calculation + $tot = $tot_l + $tot_d; + + if ($tot === 0) { + return ''; + } + + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_living = $color_living ?? 'ffffff'; + $color_dead = $color_dead ?? 'cccccc'; + + $sizes = explode('x', $size); + + $chd = $this->arrayToExtendedEncoding([ + intdiv(4095 * $tot_l, $tot), + intdiv(4095 * $tot_d, $tot), + ]); + + $per_l = $this->individualRepository->totalLivingPercentage(); + $per_d = $this->individualRepository->totalDeceasedPercentage(); + + $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; + + $colors = [$color_living, $color_dead]; + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => $chart_title, + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartNoChildrenFamilies.php b/app/Statistics/Google/ChartNoChildrenFamilies.php new file mode 100644 index 0000000000..725f30849a --- /dev/null +++ b/app/Statistics/Google/ChartNoChildrenFamilies.php @@ -0,0 +1,177 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Helper\Century; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\JoinClause; + +/** + * + */ +class ChartNoChildrenFamilies extends AbstractGoogle +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Century + */ + private $centuryHelper; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->centuryHelper = new Century(); + } + + /** + * Returns the related database records. + * + * @param int $year1 + * @param int $year2 + * + * @return \stdClass[] + */ + private function queryRecords(int $year1, int $year2): array + { + $query = DB::table('families') + ->selectRaw('FLOOR(d_year / 100 + 1) AS century') + ->selectRaw('COUNT(*) AS count') + ->join('dates', function (JoinClause $join) { + $join->on('d_file', '=', 'f_file') + ->on('d_gid', '=', 'f_id'); + }) + ->where('f_file', '=', $this->tree->id()) + ->where('f_numchil', '=', 0) + ->where('d_fact', '=', 'MARR') + ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) + ->groupBy(['century']) + ->orderBy('century'); + + if ($year1 >= 0 && $year2 >= 0) { + $query->whereBetween('d_year', [$year1, $year2]); + } + + return $query->get()->all(); + } + + /** + * Create a chart of children with no families. + * + * @param int $no_child_fam The number of families with no children + * @param string $size + * @param int $year1 + * @param int $year2 + * + * @return string + */ + public function chartNoChildrenFamilies( + int $no_child_fam, + string $size = '220x200', + int $year1 = -1, + int $year2 = -1 + ): string { + $sizes = explode('x', $size); + $max = 0; + $tot = 0; + $rows = $this->queryRecords($year1, $year2); + + if (empty($rows)) { + return ''; + } + + foreach ($rows as $values) { + $values->count = (int) $values->count; + + if ($max < $values->count) { + $max = $values->count; + } + $tot += $values->count; + } + + $unknown = $no_child_fam - $tot; + + if ($unknown > $max) { + $max = $unknown; + } + + $chm = ''; + $chxl = '0:|'; + $i = 0; + $counts = []; + + foreach ($rows as $values) { + $chxl .= $this->centuryHelper->centuryName((int) $values->century) . '|'; + $counts[] = intdiv(4095 * $values->count, $max + 1); + $chm .= 't' . $values->count . ',000000,0,' . $i . ',11,1|'; + $i++; + } + + $counts[] = intdiv(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 = ($max + 1); $d > 0; $d--) { + if (($max + 1) < ($d * 10 + 1) && fmod($max + 1, $d) === 0) { + $step = $d; + } + } + + if ($step === ($max + 1)) { + for ($d = $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') . '|'; + + $chart_url = 'https://chart.googleapis.com/chart?cht=bvg&chs=' . $sizes[0] . 'x' . $sizes[1] + . '&chf=bg,s,ffffff00|c,s,ffffff00&chm=D,FF0000,0,0:' + . ($i - 1) . ',3,1|' . $chm . '&chd=e:' + . $chd . '&chco=0000FF,ffffff00&chbh=30,3&chxt=x,x,y,y&chxl=' + . rawurlencode($chxl); + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => I18N::translate('Number of families without children'), + 'chart_url' => $chart_url, + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Google/ChartSex.php b/app/Statistics/Google/ChartSex.php new file mode 100644 index 0000000000..ac89171e23 --- /dev/null +++ b/app/Statistics/Google/ChartSex.php @@ -0,0 +1,134 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Google; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\AbstractGoogle; +use Fisharebest\Webtrees\Statistics\Repository\IndividualRepository; +use Fisharebest\Webtrees\Theme; +use Fisharebest\Webtrees\Tree; + +/** + * + */ +class ChartSex extends AbstractGoogle +{ + /** + * @var IndividualRepository + */ + private $individualRepository; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->individualRepository = new IndividualRepository($tree); + } + + /** + * Generate a chart showing sex distribution. + * + * @param int $tot_m The total number of male individuals + * @param int $tot_f The total number of female individuals + * @param int $tot_u The total number of unknown individuals + * @param string|null $size + * @param string|null $color_female + * @param string|null $color_male + * @param string|null $color_unknown + * + * @return string + */ + public function chartSex( + int $tot_m, + int $tot_f, + int $tot_u, + string $size = null, + string $color_female = null, + string $color_male = null, + string $color_unknown = null + ): string { + $chart_x = Theme::theme()->parameter('stats-small-chart-x'); + $chart_y = Theme::theme()->parameter('stats-small-chart-y'); + + $size = $size ?? ($chart_x . 'x' . $chart_y); + $color_female = $color_female ?? 'ffd1dc'; + $color_male = $color_male ?? '84beff'; + $color_unknown = $color_unknown ?? '777777'; + + $sizes = explode('x', $size); + + // Raw data - for calculation + $tot = $tot_f + $tot_m + $tot_u; + + // I18N data - for display + $per_f = $this->individualRepository->totalSexFemalesPercentage(); + $per_m = $this->individualRepository->totalSexMalesPercentage(); + $per_u = $this->individualRepository->totalSexUnknownPercentage(); + + if ($tot === 0) { + return ''; + } + + if ($tot_u > 0) { + $chd = $this->arrayToExtendedEncoding([ + intdiv(4095 * $tot_u, $tot), + intdiv(4095 * $tot_f, $tot), + intdiv(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; + + $colors = [$color_unknown, $color_female, $color_male]; + } else { + $chd = $this->arrayToExtendedEncoding([ + intdiv(4095 * $tot_f, $tot), + intdiv(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; + + $colors = [$color_female, $color_male]; + } + + return view( + 'statistics/other/chart-google', + [ + 'chart_title' => $chart_title, + 'chart_url' => $this->getPieChartUrl($chd, $size, $colors, $chl), + 'sizes' => $sizes, + ] + ); + } +} diff --git a/app/Statistics/Helper/Century.php b/app/Statistics/Helper/Century.php new file mode 100644 index 0000000000..5db201b8bd --- /dev/null +++ b/app/Statistics/Helper/Century.php @@ -0,0 +1,46 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Helper; + +use Fisharebest\Webtrees\I18N; +use NumberFormatter; + +/** + * + */ +class Century +{ + /** + * Century name, English => 21st, Polish => XXI, etc. + * + * @param int $century + * + * @return string + */ + public function centuryName(int $century): string + { + if ($century < 0) { + return I18N::translate('%s BCE', $this->centuryName(-$century)); + } + + $nf = new NumberFormatter('en_US', NumberFormatter::ORDINAL); + $suffix = $nf->format($century); + + return strip_tags(I18N::translateContext('CENTURY', $suffix)); + } +} diff --git a/app/Statistics/Helper/Country.php b/app/Statistics/Helper/Country.php new file mode 100644 index 0000000000..83d63984fc --- /dev/null +++ b/app/Statistics/Helper/Country.php @@ -0,0 +1,802 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Helper; + +use Fisharebest\Webtrees\I18N; + +/** + * Country codes and names. + */ +class Country +{ + /** + * Country codes and names + * + * @return string[] + */ + public function getAllCountries(): array + { + return [ + /* I18N: Name of a country or state */ + '???' => I18N::translate('Unknown'), + /* I18N: Name of a country or state */ + 'ABW' => I18N::translate('Aruba'), + /* I18N: Name of a country or state */ + 'AFG' => I18N::translate('Afghanistan'), + /* I18N: Name of a country or state */ + 'AGO' => I18N::translate('Angola'), + /* I18N: Name of a country or state */ + 'AIA' => I18N::translate('Anguilla'), + /* I18N: Name of a country or state */ + 'ALA' => I18N::translate('Aland Islands'), + /* I18N: Name of a country or state */ + 'ALB' => I18N::translate('Albania'), + /* I18N: Name of a country or state */ + 'AND' => I18N::translate('Andorra'), + /* I18N: Name of a country or state */ + 'ARE' => I18N::translate('United Arab Emirates'), + /* I18N: Name of a country or state */ + 'ARG' => I18N::translate('Argentina'), + /* I18N: Name of a country or state */ + 'ARM' => I18N::translate('Armenia'), + /* I18N: Name of a country or state */ + 'ASM' => I18N::translate('American Samoa'), + /* I18N: Name of a country or state */ + 'ATA' => I18N::translate('Antarctica'), + /* I18N: Name of a country or state */ + 'ATF' => I18N::translate('French Southern Territories'), + /* I18N: Name of a country or state */ + 'ATG' => I18N::translate('Antigua and Barbuda'), + /* I18N: Name of a country or state */ + 'AUS' => I18N::translate('Australia'), + /* I18N: Name of a country or state */ + 'AUT' => I18N::translate('Austria'), + /* I18N: Name of a country or state */ + 'AZE' => I18N::translate('Azerbaijan'), + /* I18N: Name of a country or state */ + 'AZR' => I18N::translate('Azores'), + /* I18N: Name of a country or state */ + 'BDI' => I18N::translate('Burundi'), + /* I18N: Name of a country or state */ + 'BEL' => I18N::translate('Belgium'), + /* I18N: Name of a country or state */ + 'BEN' => I18N::translate('Benin'), + // BES => Bonaire, Sint Eustatius and Saba + /* I18N: Name of a country or state */ + 'BFA' => I18N::translate('Burkina Faso'), + /* I18N: Name of a country or state */ + 'BGD' => I18N::translate('Bangladesh'), + /* I18N: Name of a country or state */ + 'BGR' => I18N::translate('Bulgaria'), + /* I18N: Name of a country or state */ + 'BHR' => I18N::translate('Bahrain'), + /* I18N: Name of a country or state */ + 'BHS' => I18N::translate('Bahamas'), + /* I18N: Name of a country or state */ + 'BIH' => I18N::translate('Bosnia and Herzegovina'), + // BLM => Saint Barthélemy + /* I18N: Name of a country or state */ + 'BLR' => I18N::translate('Belarus'), + /* I18N: Name of a country or state */ + 'BLZ' => I18N::translate('Belize'), + /* I18N: Name of a country or state */ + 'BMU' => I18N::translate('Bermuda'), + /* I18N: Name of a country or state */ + 'BOL' => I18N::translate('Bolivia'), + /* I18N: Name of a country or state */ + 'BRA' => I18N::translate('Brazil'), + /* I18N: Name of a country or state */ + 'BRB' => I18N::translate('Barbados'), + /* I18N: Name of a country or state */ + 'BRN' => I18N::translate('Brunei Darussalam'), + /* I18N: Name of a country or state */ + 'BTN' => I18N::translate('Bhutan'), + /* I18N: Name of a country or state */ + 'BVT' => I18N::translate('Bouvet Island'), + /* I18N: Name of a country or state */ + 'BWA' => I18N::translate('Botswana'), + /* I18N: Name of a country or state */ + 'CAF' => I18N::translate('Central African Republic'), + /* I18N: Name of a country or state */ + 'CAN' => I18N::translate('Canada'), + /* I18N: Name of a country or state */ + 'CCK' => I18N::translate('Cocos (Keeling) Islands'), + /* I18N: Name of a country or state */ + 'CHE' => I18N::translate('Switzerland'), + /* I18N: Name of a country or state */ + 'CHL' => I18N::translate('Chile'), + /* I18N: Name of a country or state */ + 'CHN' => I18N::translate('China'), + /* I18N: Name of a country or state */ + 'CIV' => I18N::translate('Cote d’Ivoire'), + /* I18N: Name of a country or state */ + 'CMR' => I18N::translate('Cameroon'), + /* I18N: Name of a country or state */ + 'COD' => I18N::translate('Democratic Republic of the Congo'), + /* I18N: Name of a country or state */ + 'COG' => I18N::translate('Republic of the Congo'), + /* I18N: Name of a country or state */ + 'COK' => I18N::translate('Cook Islands'), + /* I18N: Name of a country or state */ + 'COL' => I18N::translate('Colombia'), + /* I18N: Name of a country or state */ + 'COM' => I18N::translate('Comoros'), + /* I18N: Name of a country or state */ + 'CPV' => I18N::translate('Cape Verde'), + /* I18N: Name of a country or state */ + 'CRI' => I18N::translate('Costa Rica'), + /* I18N: Name of a country or state */ + 'CUB' => I18N::translate('Cuba'), + // CUW => Curaçao + /* I18N: Name of a country or state */ + 'CXR' => I18N::translate('Christmas Island'), + /* I18N: Name of a country or state */ + 'CYM' => I18N::translate('Cayman Islands'), + /* I18N: Name of a country or state */ + 'CYP' => I18N::translate('Cyprus'), + /* I18N: Name of a country or state */ + 'CZE' => I18N::translate('Czech Republic'), + /* I18N: Name of a country or state */ + 'DEU' => I18N::translate('Germany'), + /* I18N: Name of a country or state */ + 'DJI' => I18N::translate('Djibouti'), + /* I18N: Name of a country or state */ + 'DMA' => I18N::translate('Dominica'), + /* I18N: Name of a country or state */ + 'DNK' => I18N::translate('Denmark'), + /* I18N: Name of a country or state */ + 'DOM' => I18N::translate('Dominican Republic'), + /* I18N: Name of a country or state */ + 'DZA' => I18N::translate('Algeria'), + /* I18N: Name of a country or state */ + 'ECU' => I18N::translate('Ecuador'), + /* I18N: Name of a country or state */ + 'EGY' => I18N::translate('Egypt'), + /* I18N: Name of a country or state */ + 'ENG' => I18N::translate('England'), + /* I18N: Name of a country or state */ + 'ERI' => I18N::translate('Eritrea'), + /* I18N: Name of a country or state */ + 'ESH' => I18N::translate('Western Sahara'), + /* I18N: Name of a country or state */ + 'ESP' => I18N::translate('Spain'), + /* I18N: Name of a country or state */ + 'EST' => I18N::translate('Estonia'), + /* I18N: Name of a country or state */ + 'ETH' => I18N::translate('Ethiopia'), + /* I18N: Name of a country or state */ + 'FIN' => I18N::translate('Finland'), + /* I18N: Name of a country or state */ + 'FJI' => I18N::translate('Fiji'), + /* I18N: Name of a country or state */ + 'FLD' => I18N::translate('Flanders'), + /* I18N: Name of a country or state */ + 'FLK' => I18N::translate('Falkland Islands'), + /* I18N: Name of a country or state */ + 'FRA' => I18N::translate('France'), + /* I18N: Name of a country or state */ + 'FRO' => I18N::translate('Faroe Islands'), + /* I18N: Name of a country or state */ + 'FSM' => I18N::translate('Micronesia'), + /* I18N: Name of a country or state */ + 'GAB' => I18N::translate('Gabon'), + /* I18N: Name of a country or state */ + 'GBR' => I18N::translate('United Kingdom'), + /* I18N: Name of a country or state */ + 'GEO' => I18N::translate('Georgia'), + /* I18N: Name of a country or state */ + 'GGY' => I18N::translate('Guernsey'), + /* I18N: Name of a country or state */ + 'GHA' => I18N::translate('Ghana'), + /* I18N: Name of a country or state */ + 'GIB' => I18N::translate('Gibraltar'), + /* I18N: Name of a country or state */ + 'GIN' => I18N::translate('Guinea'), + /* I18N: Name of a country or state */ + 'GLP' => I18N::translate('Guadeloupe'), + /* I18N: Name of a country or state */ + 'GMB' => I18N::translate('Gambia'), + /* I18N: Name of a country or state */ + 'GNB' => I18N::translate('Guinea-Bissau'), + /* I18N: Name of a country or state */ + 'GNQ' => I18N::translate('Equatorial Guinea'), + /* I18N: Name of a country or state */ + 'GRC' => I18N::translate('Greece'), + /* I18N: Name of a country or state */ + 'GRD' => I18N::translate('Grenada'), + /* I18N: Name of a country or state */ + 'GRL' => I18N::translate('Greenland'), + /* I18N: Name of a country or state */ + 'GTM' => I18N::translate('Guatemala'), + /* I18N: Name of a country or state */ + 'GUF' => I18N::translate('French Guiana'), + /* I18N: Name of a country or state */ + 'GUM' => I18N::translate('Guam'), + /* I18N: Name of a country or state */ + 'GUY' => I18N::translate('Guyana'), + /* I18N: Name of a country or state */ + 'HKG' => I18N::translate('Hong Kong'), + /* I18N: Name of a country or state */ + 'HMD' => I18N::translate('Heard Island and McDonald Islands'), + /* I18N: Name of a country or state */ + 'HND' => I18N::translate('Honduras'), + /* I18N: Name of a country or state */ + 'HRV' => I18N::translate('Croatia'), + /* I18N: Name of a country or state */ + 'HTI' => I18N::translate('Haiti'), + /* I18N: Name of a country or state */ + 'HUN' => I18N::translate('Hungary'), + /* I18N: Name of a country or state */ + 'IDN' => I18N::translate('Indonesia'), + /* I18N: Name of a country or state */ + 'IND' => I18N::translate('India'), + /* I18N: Name of a country or state */ + 'IOM' => I18N::translate('Isle of Man'), + /* I18N: Name of a country or state */ + 'IOT' => I18N::translate('British Indian Ocean Territory'), + /* I18N: Name of a country or state */ + 'IRL' => I18N::translate('Ireland'), + /* I18N: Name of a country or state */ + 'IRN' => I18N::translate('Iran'), + /* I18N: Name of a country or state */ + 'IRQ' => I18N::translate('Iraq'), + /* I18N: Name of a country or state */ + 'ISL' => I18N::translate('Iceland'), + /* I18N: Name of a country or state */ + 'ISR' => I18N::translate('Israel'), + /* I18N: Name of a country or state */ + 'ITA' => I18N::translate('Italy'), + /* I18N: Name of a country or state */ + 'JAM' => I18N::translate('Jamaica'), + //'JEY' => Jersey + /* I18N: Name of a country or state */ + 'JOR' => I18N::translate('Jordan'), + /* I18N: Name of a country or state */ + 'JPN' => I18N::translate('Japan'), + /* I18N: Name of a country or state */ + 'KAZ' => I18N::translate('Kazakhstan'), + /* I18N: Name of a country or state */ + 'KEN' => I18N::translate('Kenya'), + /* I18N: Name of a country or state */ + 'KGZ' => I18N::translate('Kyrgyzstan'), + /* I18N: Name of a country or state */ + 'KHM' => I18N::translate('Cambodia'), + /* I18N: Name of a country or state */ + 'KIR' => I18N::translate('Kiribati'), + /* I18N: Name of a country or state */ + 'KNA' => I18N::translate('Saint Kitts and Nevis'), + /* I18N: Name of a country or state */ + 'KOR' => I18N::translate('Korea'), + /* I18N: Name of a country or state */ + 'KWT' => I18N::translate('Kuwait'), + /* I18N: Name of a country or state */ + 'LAO' => I18N::translate('Laos'), + /* I18N: Name of a country or state */ + 'LBN' => I18N::translate('Lebanon'), + /* I18N: Name of a country or state */ + 'LBR' => I18N::translate('Liberia'), + /* I18N: Name of a country or state */ + 'LBY' => I18N::translate('Libya'), + /* I18N: Name of a country or state */ + 'LCA' => I18N::translate('Saint Lucia'), + /* I18N: Name of a country or state */ + 'LIE' => I18N::translate('Liechtenstein'), + /* I18N: Name of a country or state */ + 'LKA' => I18N::translate('Sri Lanka'), + /* I18N: Name of a country or state */ + 'LSO' => I18N::translate('Lesotho'), + /* I18N: Name of a country or state */ + 'LTU' => I18N::translate('Lithuania'), + /* I18N: Name of a country or state */ + 'LUX' => I18N::translate('Luxembourg'), + /* I18N: Name of a country or state */ + 'LVA' => I18N::translate('Latvia'), + /* I18N: Name of a country or state */ + 'MAC' => I18N::translate('Macau'), + // MAF => Saint Martin + /* I18N: Name of a country or state */ + 'MAR' => I18N::translate('Morocco'), + /* I18N: Name of a country or state */ + 'MCO' => I18N::translate('Monaco'), + /* I18N: Name of a country or state */ + 'MDA' => I18N::translate('Moldova'), + /* I18N: Name of a country or state */ + 'MDG' => I18N::translate('Madagascar'), + /* I18N: Name of a country or state */ + 'MDV' => I18N::translate('Maldives'), + /* I18N: Name of a country or state */ + 'MEX' => I18N::translate('Mexico'), + /* I18N: Name of a country or state */ + 'MHL' => I18N::translate('Marshall Islands'), + /* I18N: Name of a country or state */ + 'MKD' => I18N::translate('Macedonia'), + /* I18N: Name of a country or state */ + 'MLI' => I18N::translate('Mali'), + /* I18N: Name of a country or state */ + 'MLT' => I18N::translate('Malta'), + /* I18N: Name of a country or state */ + 'MMR' => I18N::translate('Myanmar'), + /* I18N: Name of a country or state */ + 'MNG' => I18N::translate('Mongolia'), + /* I18N: Name of a country or state */ + 'MNP' => I18N::translate('Northern Mariana Islands'), + /* I18N: Name of a country or state */ + 'MNT' => I18N::translate('Montenegro'), + /* I18N: Name of a country or state */ + 'MOZ' => I18N::translate('Mozambique'), + /* I18N: Name of a country or state */ + 'MRT' => I18N::translate('Mauritania'), + /* I18N: Name of a country or state */ + 'MSR' => I18N::translate('Montserrat'), + /* I18N: Name of a country or state */ + 'MTQ' => I18N::translate('Martinique'), + /* I18N: Name of a country or state */ + 'MUS' => I18N::translate('Mauritius'), + /* I18N: Name of a country or state */ + 'MWI' => I18N::translate('Malawi'), + /* I18N: Name of a country or state */ + 'MYS' => I18N::translate('Malaysia'), + /* I18N: Name of a country or state */ + 'MYT' => I18N::translate('Mayotte'), + /* I18N: Name of a country or state */ + 'NAM' => I18N::translate('Namibia'), + /* I18N: Name of a country or state */ + 'NCL' => I18N::translate('New Caledonia'), + /* I18N: Name of a country or state */ + 'NER' => I18N::translate('Niger'), + /* I18N: Name of a country or state */ + 'NFK' => I18N::translate('Norfolk Island'), + /* I18N: Name of a country or state */ + 'NGA' => I18N::translate('Nigeria'), + /* I18N: Name of a country or state */ + 'NIC' => I18N::translate('Nicaragua'), + /* I18N: Name of a country or state */ + 'NIR' => I18N::translate('Northern Ireland'), + /* I18N: Name of a country or state */ + 'NIU' => I18N::translate('Niue'), + /* I18N: Name of a country or state */ + 'NLD' => I18N::translate('Netherlands'), + /* I18N: Name of a country or state */ + 'NOR' => I18N::translate('Norway'), + /* I18N: Name of a country or state */ + 'NPL' => I18N::translate('Nepal'), + /* I18N: Name of a country or state */ + 'NRU' => I18N::translate('Nauru'), + /* I18N: Name of a country or state */ + 'NZL' => I18N::translate('New Zealand'), + /* I18N: Name of a country or state */ + 'OMN' => I18N::translate('Oman'), + /* I18N: Name of a country or state */ + 'PAK' => I18N::translate('Pakistan'), + /* I18N: Name of a country or state */ + 'PAN' => I18N::translate('Panama'), + /* I18N: Name of a country or state */ + 'PCN' => I18N::translate('Pitcairn'), + /* I18N: Name of a country or state */ + 'PER' => I18N::translate('Peru'), + /* I18N: Name of a country or state */ + 'PHL' => I18N::translate('Philippines'), + /* I18N: Name of a country or state */ + 'PLW' => I18N::translate('Palau'), + /* I18N: Name of a country or state */ + 'PNG' => I18N::translate('Papua New Guinea'), + /* I18N: Name of a country or state */ + 'POL' => I18N::translate('Poland'), + /* I18N: Name of a country or state */ + 'PRI' => I18N::translate('Puerto Rico'), + /* I18N: Name of a country or state */ + 'PRK' => I18N::translate('North Korea'), + /* I18N: Name of a country or state */ + 'PRT' => I18N::translate('Portugal'), + /* I18N: Name of a country or state */ + 'PRY' => I18N::translate('Paraguay'), + /* I18N: Name of a country or state */ + 'PSE' => I18N::translate('Occupied Palestinian Territory'), + /* I18N: Name of a country or state */ + 'PYF' => I18N::translate('French Polynesia'), + /* I18N: Name of a country or state */ + 'QAT' => I18N::translate('Qatar'), + /* I18N: Name of a country or state */ + 'REU' => I18N::translate('Reunion'), + /* I18N: Name of a country or state */ + 'ROM' => I18N::translate('Romania'), + /* I18N: Name of a country or state */ + 'RUS' => I18N::translate('Russia'), + /* I18N: Name of a country or state */ + 'RWA' => I18N::translate('Rwanda'), + /* I18N: Name of a country or state */ + 'SAU' => I18N::translate('Saudi Arabia'), + /* I18N: Name of a country or state */ + 'SCT' => I18N::translate('Scotland'), + /* I18N: Name of a country or state */ + 'SDN' => I18N::translate('Sudan'), + /* I18N: Name of a country or state */ + 'SEA' => I18N::translate('At sea'), + /* I18N: Name of a country or state */ + 'SEN' => I18N::translate('Senegal'), + /* I18N: Name of a country or state */ + 'SER' => I18N::translate('Serbia'), + /* I18N: Name of a country or state */ + 'SGP' => I18N::translate('Singapore'), + /* I18N: Name of a country or state */ + 'SGS' => I18N::translate('South Georgia and the South Sandwich Islands'), + /* I18N: Name of a country or state */ + 'SHN' => I18N::translate('Saint Helena'), + /* I18N: Name of a country or state */ + 'SJM' => I18N::translate('Svalbard and Jan Mayen'), + /* I18N: Name of a country or state */ + 'SLB' => I18N::translate('Solomon Islands'), + /* I18N: Name of a country or state */ + 'SLE' => I18N::translate('Sierra Leone'), + /* I18N: Name of a country or state */ + 'SLV' => I18N::translate('El Salvador'), + /* I18N: Name of a country or state */ + 'SMR' => I18N::translate('San Marino'), + /* I18N: Name of a country or state */ + 'SOM' => I18N::translate('Somalia'), + /* I18N: Name of a country or state */ + 'SPM' => I18N::translate('Saint Pierre and Miquelon'), + /* I18N: Name of a country or state */ + 'SSD' => I18N::translate('South Sudan'), + /* I18N: Name of a country or state */ + 'STP' => I18N::translate('Sao Tome and Principe'), + /* I18N: Name of a country or state */ + 'SUR' => I18N::translate('Suriname'), + /* I18N: Name of a country or state */ + 'SVK' => I18N::translate('Slovakia'), + /* I18N: Name of a country or state */ + 'SVN' => I18N::translate('Slovenia'), + /* I18N: Name of a country or state */ + 'SWE' => I18N::translate('Sweden'), + /* I18N: Name of a country or state */ + 'SWZ' => I18N::translate('Swaziland'), + // SXM => Sint Maarten + /* I18N: Name of a country or state */ + 'SYC' => I18N::translate('Seychelles'), + /* I18N: Name of a country or state */ + 'SYR' => I18N::translate('Syria'), + /* I18N: Name of a country or state */ + 'TCA' => I18N::translate('Turks and Caicos Islands'), + /* I18N: Name of a country or state */ + 'TCD' => I18N::translate('Chad'), + /* I18N: Name of a country or state */ + 'TGO' => I18N::translate('Togo'), + /* I18N: Name of a country or state */ + 'THA' => I18N::translate('Thailand'), + /* I18N: Name of a country or state */ + 'TJK' => I18N::translate('Tajikistan'), + /* I18N: Name of a country or state */ + 'TKL' => I18N::translate('Tokelau'), + /* I18N: Name of a country or state */ + 'TKM' => I18N::translate('Turkmenistan'), + /* I18N: Name of a country or state */ + 'TLS' => I18N::translate('Timor-Leste'), + /* I18N: Name of a country or state */ + 'TON' => I18N::translate('Tonga'), + /* I18N: Name of a country or state */ + 'TTO' => I18N::translate('Trinidad and Tobago'), + /* I18N: Name of a country or state */ + 'TUN' => I18N::translate('Tunisia'), + /* I18N: Name of a country or state */ + 'TUR' => I18N::translate('Turkey'), + /* I18N: Name of a country or state */ + 'TUV' => I18N::translate('Tuvalu'), + /* I18N: Name of a country or state */ + 'TWN' => I18N::translate('Taiwan'), + /* I18N: Name of a country or state */ + 'TZA' => I18N::translate('Tanzania'), + /* I18N: Name of a country or state */ + 'UGA' => I18N::translate('Uganda'), + /* I18N: Name of a country or state */ + 'UKR' => I18N::translate('Ukraine'), + /* I18N: Name of a country or state */ + 'UMI' => I18N::translate('US Minor Outlying Islands'), + /* I18N: Name of a country or state */ + 'URY' => I18N::translate('Uruguay'), + /* I18N: Name of a country or state */ + 'USA' => I18N::translate('United States'), + /* I18N: Name of a country or state */ + 'UZB' => I18N::translate('Uzbekistan'), + /* I18N: Name of a country or state */ + 'VAT' => I18N::translate('Vatican City'), + /* I18N: Name of a country or state */ + 'VCT' => I18N::translate('Saint Vincent and the Grenadines'), + /* I18N: Name of a country or state */ + 'VEN' => I18N::translate('Venezuela'), + /* I18N: Name of a country or state */ + 'VGB' => I18N::translate('British Virgin Islands'), + /* I18N: Name of a country or state */ + 'VIR' => I18N::translate('US Virgin Islands'), + /* I18N: Name of a country or state */ + 'VNM' => I18N::translate('Vietnam'), + /* I18N: Name of a country or state */ + 'VUT' => I18N::translate('Vanuatu'), + /* I18N: Name of a country or state */ + 'WLF' => I18N::translate('Wallis and Futuna'), + /* I18N: Name of a country or state */ + 'WLS' => I18N::translate('Wales'), + /* I18N: Name of a country or state */ + 'WSM' => I18N::translate('Samoa'), + /* I18N: Name of a country or state */ + 'YEM' => I18N::translate('Yemen'), + /* I18N: Name of a country or state */ + 'ZAF' => I18N::translate('South Africa'), + /* I18N: Name of a country or state */ + 'ZMB' => I18N::translate('Zambia'), + /* I18N: Name of a country or state */ + 'ZWE' => I18N::translate('Zimbabwe'), + ]; + } + + /** + * 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(): array + { + return [ + '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', + ]; + } +} diff --git a/app/Statistics/Helper/Sql.php b/app/Statistics/Helper/Sql.php new file mode 100644 index 0000000000..8687b39475 --- /dev/null +++ b/app/Statistics/Helper/Sql.php @@ -0,0 +1,49 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Helper; + +use Fisharebest\Webtrees\Database; + +/** + * + */ +class Sql +{ + /** + * Run an SQL query and cache the result. + * + * @param string $sql + * + * @return \stdClass[] + */ + public static function runSql(string $sql): array + { + static $cache = []; + + $id = md5($sql); + + if (isset($cache[$id])) { + return $cache[$id]; + } + + $rows = Database::prepare($sql)->fetchAll(); + $cache[$id] = $rows; + + return $rows; + } +} diff --git a/app/Statistics/Repository/BrowserRepository.php b/app/Statistics/Repository/BrowserRepository.php new file mode 100644 index 0000000000..fd152c7f0c --- /dev/null +++ b/app/Statistics/Repository/BrowserRepository.php @@ -0,0 +1,56 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Functions\FunctionsDate; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\BrowserRepositoryInterface; + +/** + * A repository providing methods for browser related statistics. + */ +class BrowserRepository implements BrowserRepositoryInterface +{ + /** + * @inheritDoc + */ + public function browserDate(): string + { + // TODO: Duplicates ServerRepository::serverDate + return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display(); + } + + /** + * @inheritDoc + */ + public function browserTime(): string + { + return date( + str_replace('%', '', I18N::timeFormat()), + WT_TIMESTAMP + WT_TIMESTAMP_OFFSET + ); + } + + /** + * @inheritDoc + */ + public function browserTimezone(): string + { + return date('T', WT_TIMESTAMP + WT_TIMESTAMP_OFFSET); + } +} diff --git a/app/Statistics/Repository/ContactRepository.php b/app/Statistics/Repository/ContactRepository.php new file mode 100644 index 0000000000..b27a502782 --- /dev/null +++ b/app/Statistics/Repository/ContactRepository.php @@ -0,0 +1,82 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\ContactRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Fisharebest\Webtrees\User; +use Symfony\Component\HttpFoundation\Request; + +/** + * A repository providing methods for contact related statistics. + */ +class ContactRepository implements ContactRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * @inheritDoc + */ + public function contactWebmaster(): string + { + $user_id = $this->tree->getPreference('WEBMASTER_USER_ID'); + $user = User::find((int) $user_id); + + if ($user instanceof User) { + return view('modules/contact-links/contact', [ + 'request' => app()->make(Request::class), + 'user' => $user, + 'tree' => $this->tree, + ]); + } + + return ''; + } + + /** + * @inheritDoc + */ + public function contactGedcom(): string + { + $user_id = $this->tree->getPreference('CONTACT_USER_ID'); + $user = User::find((int) $user_id); + + if ($user instanceof User) { + return view('modules/contact-links/contact', [ + 'request' => app()->make(Request::class), + 'user' => $user, + 'tree' => $this->tree, + ]); + } + + return ''; + } +} diff --git a/app/Statistics/Repository/EventRepository.php b/app/Statistics/Repository/EventRepository.php new file mode 100644 index 0000000000..e8e04f82e4 --- /dev/null +++ b/app/Statistics/Repository/EventRepository.php @@ -0,0 +1,430 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Date; +use Fisharebest\Webtrees\Functions\FunctionsPrint; +use Fisharebest\Webtrees\Gedcom; +use Fisharebest\Webtrees\GedcomRecord; +use Fisharebest\Webtrees\GedcomTag; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\EventRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Query\Builder; + +/** + * A repository providing methods for event related statistics. + */ +class EventRepository implements EventRepositoryInterface +{ + /** + * Sorting directions. + */ + private const SORT_ASC = 'ASC'; + private const SORT_DESC = 'DESC'; + + /** + * Event facts. + */ + private const EVENT_BIRTH = 'BIRT'; + private const EVENT_DEATH = 'DEAT'; + private const EVENT_MARRIAGE = 'MARR'; + private const EVENT_DIVORCE = 'DIV'; + private const EVENT_ADOPTION = 'ADOP'; + private const EVENT_BURIAL = 'BURI'; + private const EVENT_CENSUS = 'CENS'; + + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Returns the total number of a given list of events (with dates). + * + * @param array $events The list of events to count (e.g. BIRT, DEAT, ...) + * + * @return int + */ + private function getEventCount(array $events = []): int + { + $query = DB::table('dates') + ->where('d_file', '=', $this->tree->id()); + + $no_types = [ + 'HEAD', + 'CHAN', + ]; + + if ($events) { + $types = []; + + foreach ($events as $type) { + if (strncmp($type, '!', 1) === 0) { + $no_types[] = substr($type, 1); + } else { + $types[] = $type; + } + } + + if ($types) { + $query->whereIn('d_fact', $types); + } + } + + return $query->whereNotIn('d_fact', $no_types) + ->count(); + } + + /** + * @inheritDoc + */ + public function totalEvents(array $events = []): string + { + return I18N::number( + $this->getEventCount($events) + ); + } + + /** + * @inheritDoc + */ + public function totalEventsBirth(): string + { + return $this->totalEvents(Gedcom::BIRTH_EVENTS); + } + + /** + * @inheritDoc + */ + public function totalBirths(): string + { + return $this->totalEvents([self::EVENT_BIRTH]); + } + + /** + * @inheritDoc + */ + public function totalEventsDeath(): string + { + return $this->totalEvents(Gedcom::DEATH_EVENTS); + } + + /** + * @inheritDoc + */ + public function totalDeaths(): string + { + return $this->totalEvents([self::EVENT_DEATH]); + } + + /** + * @inheritDoc + */ + public function totalEventsMarriage(): string + { + return $this->totalEvents(Gedcom::MARRIAGE_EVENTS); + } + + /** + * @inheritDoc + */ + public function totalMarriages(): string + { + return $this->totalEvents([self::EVENT_MARRIAGE]); + } + + /** + * @inheritDoc + */ + public function totalEventsDivorce(): string + { + return $this->totalEvents(Gedcom::DIVORCE_EVENTS); + } + + /** + * @inheritDoc + */ + public function totalDivorces(): string + { + return $this->totalEvents([self::EVENT_DIVORCE]); + } + + /** + * Retursn the list of common facts used query the data. + * + * @return array + */ + private function getCommonFacts(): array + { + // The list of facts used to limit the query result + return array_merge( + Gedcom::BIRTH_EVENTS, + Gedcom::MARRIAGE_EVENTS, + Gedcom::DIVORCE_EVENTS, + Gedcom::DEATH_EVENTS + ); + } + + /** + * @inheritDoc + */ + public function totalEventsOther(): string + { + $no_facts = array_map( + function (string $fact) { + return '!' . $fact; + }, + $this->getCommonFacts() + ); + + return $this->totalEvents($no_facts); + } + + /** + * Returns the first/last event record from the given list of event facts. + * + * @param string $direction The sorting direction of the query (To return first or last record) + * + * @return Model|Builder|object|null + */ + private function eventQuery(string $direction) + { + return DB::table('dates') + ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type']) + ->where('d_file', '=', $this->tree->id()) + ->where('d_gid', '<>', 'HEAD') + ->whereIn('d_fact', $this->getCommonFacts()) + ->where('d_julianday1', '<>', 0) + ->orderBy('d_julianday1', $direction) + ->orderBy('d_type') + ->first(); + } + + /** + * Returns the formatted first/last occuring event. + * + * @param string $direction The sorting direction + * + * @return string + */ + private function getFirstLastEvent(string $direction): string + { + $row = $this->eventQuery($direction); + $result = ''; + + if ($row) { + $record = GedcomRecord::getInstance($row->id, $this->tree); + + if ($record && $record->canShow()) { + $result = $record->formatList(); + } else { + $result = I18N::translate('This information is private and cannot be shown.'); + } + } + + return $result; + } + + /** + * @inheritDoc + */ + public function firstEvent(): string + { + return $this->getFirstLastEvent(self::SORT_ASC); + } + + /** + * @inheritDoc + */ + public function lastEvent(): string + { + return $this->getFirstLastEvent(self::SORT_DESC); + } + + /** + * Returns the formatted year of the first/last occuring event. + * + * @param string $direction The sorting direction + * + * @return string + */ + private function getFirstLastEventYear(string $direction): string + { + $row = $this->eventQuery($direction); + + if (!$row) { + return ''; + } + + return (new Date($row->type . ' ' . $row->year)) + ->display(); + } + + /** + * @inheritDoc + */ + public function firstEventYear(): string + { + return $this->getFirstLastEventYear(self::SORT_ASC); + } + + /** + * @inheritDoc + */ + public function lastEventYear(): string + { + return $this->getFirstLastEventYear(self::SORT_DESC); + } + + /** + * Returns the formatted type of the first/last occuring event. + * + * @param string $direction The sorting direction + * + * @return string + */ + private function getFirstLastEventType(string $direction): string + { + $row = $this->eventQuery($direction); + + if ($row) { + $event_types = [ + self::EVENT_BIRTH => I18N::translate('birth'), + self::EVENT_DEATH => I18N::translate('death'), + self::EVENT_MARRIAGE => I18N::translate('marriage'), + self::EVENT_ADOPTION => I18N::translate('adoption'), + self::EVENT_BURIAL => I18N::translate('burial'), + self::EVENT_CENSUS => I18N::translate('census added'), + ]; + + return $event_types[$row->fact] ?? GedcomTag::getLabel($row->fact); + } + + return ''; + } + + /** + * @inheritDoc + */ + public function firstEventType(): string + { + return $this->getFirstLastEventType(self::SORT_ASC); + } + + /** + * @inheritDoc + */ + public function lastEventType(): string + { + return $this->getFirstLastEventType(self::SORT_DESC); + } + + /** + * Returns the formatted name of the first/last occuring event. + * + * @param string $direction The sorting direction + * + * @return string + */ + private function getFirstLastEventName(string $direction): string + { + $row = $this->eventQuery($direction); + + if ($row) { + $record = GedcomRecord::getInstance($row->id, $this->tree); + + if ($record) { + return '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>'; + } + } + + return ''; + } + + /** + * @inheritDoc + */ + public function firstEventName(): string + { + return $this->getFirstLastEventName(self::SORT_ASC); + } + + /** + * @inheritDoc + */ + public function lastEventName(): string + { + return $this->getFirstLastEventName(self::SORT_DESC); + } + + /** + * Returns the formatted place of the first/last occuring event. + * + * @param string $direction The sorting direction + * + * @return string + */ + private function getFirstLastEventPlace(string $direction): string + { + $row = $this->eventQuery($direction); + + if ($row) { + $record = GedcomRecord::getInstance($row->id, $this->tree); + $fact = null; + + if ($record) { + $fact = $record->getFirstFact($row->fact); + } + + if ($fact) { + return FunctionsPrint::formatFactPlace($fact, true, true, true); + } + } + + return I18N::translate('Private'); + } + + /** + * @inheritDoc + */ + public function firstEventPlace(): string + { + return $this->getFirstLastEventPlace(self::SORT_ASC); + } + + /** + * @inheritDoc + */ + public function lastEventPlace(): string + { + return $this->getFirstLastEventPlace(self::SORT_DESC); + } +} diff --git a/app/Statistics/Repository/FamilyDatesRepository.php b/app/Statistics/Repository/FamilyDatesRepository.php new file mode 100644 index 0000000000..2ab24e199d --- /dev/null +++ b/app/Statistics/Repository/FamilyDatesRepository.php @@ -0,0 +1,444 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Date; +use Fisharebest\Webtrees\Functions\FunctionsPrint; +use Fisharebest\Webtrees\GedcomRecord; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\FamilyDatesRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Query\Builder; + +/** + * A repository providing methods for family dates related statistics (birth, death, marriage, divorce). + */ +class FamilyDatesRepository implements FamilyDatesRepositoryInterface +{ + /** + * Sorting directions. + */ + private const SORT_MIN = 'MIN'; + private const SORT_MAX = 'MAX'; + + /** + * Event facts. + */ + private const EVENT_BIRTH = 'BIRT'; + private const EVENT_DEATH = 'DEAT'; + private const EVENT_MARRIAGE = 'MARR'; + private const EVENT_DIVORCE = 'DIV'; + + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Returns the first/last event record for the given event fact. + * + * @param string $fact + * @param string $operation + * + * @return Model|object|static|null + */ + private function eventQuery(string $fact, string $operation) + { + return DB::table('dates') + ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type']) + ->where('d_file', '=', $this->tree->id()) + ->where('d_fact', '=', $fact) + ->where('d_julianday1', '=', function (Builder $query) use ($operation, $fact) { + $query->selectRaw($operation . '(d_julianday1)') + ->from('dates') + ->where('d_file', '=', $this->tree->id()) + ->where('d_fact', '=', $fact) + ->where('d_julianday1', '<>', 0); + }) + ->first(); + } + + /** + * Returns the formatted year of the first/last occuring event. + * + * @param string $type The fact to query + * @param string $operation The sorting operation + * + * @return string + */ + private function getFirstLastEvent(string $type, string $operation): string + { + $row = $this->eventQuery($type, $operation); + $result = ''; + + if ($row) { + $record = GedcomRecord::getInstance($row->id, $this->tree); + + if ($record && $record->canShow()) { + $result = $record->formatList(); + } else { + $result = I18N::translate('This information is private and cannot be shown.'); + } + } + + return $result; + } + + /** + * @inheritDoc + */ + public function firstBirth(): string + { + return $this->getFirstLastEvent(self::EVENT_BIRTH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastBirth(): string + { + return $this->getFirstLastEvent(self::EVENT_BIRTH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDeath(): string + { + return $this->getFirstLastEvent(self::EVENT_DEATH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDeath(): string + { + return $this->getFirstLastEvent(self::EVENT_DEATH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstMarriage(): string + { + return $this->getFirstLastEvent(self::EVENT_MARRIAGE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastMarriage(): string + { + return $this->getFirstLastEvent(self::EVENT_MARRIAGE, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDivorce(): string + { + return $this->getFirstLastEvent(self::EVENT_DIVORCE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDivorce(): string + { + return $this->getFirstLastEvent(self::EVENT_DIVORCE, self::SORT_MAX); + } + + /** + * Returns the formatted year of the first/last occuring event. + * + * @param string $type The fact to query + * @param string $operation The sorting operation + * + * @return string + */ + private function getFirstLastEventYear(string $type, string $operation): string + { + $row = $this->eventQuery($type, $operation); + + if (!$row) { + return ''; + } + + if ($row->year < 0) { + $row->year = abs($row->year) . ' B.C.'; + } + + return (new Date($row->type . ' ' . $row->year)) + ->display(); + } + + /** + * @inheritDoc + */ + public function firstBirthYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_BIRTH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastBirthYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_BIRTH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDeathYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_DEATH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDeathYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_DEATH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstMarriageYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_MARRIAGE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastMarriageYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_MARRIAGE, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDivorceYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_DIVORCE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDivorceYear(): string + { + return $this->getFirstLastEventYear(self::EVENT_DIVORCE, self::SORT_MAX); + } + + /** + * Returns the formatted name of the first/last occuring event. + * + * @param string $type The fact to query + * @param string $operation The sorting operation + * + * @return string + */ + private function getFirstLastEventName(string $type, string $operation): string + { + $row = $this->eventQuery($type, $operation); + + if ($row) { + $record = GedcomRecord::getInstance($row->id, $this->tree); + + if ($record) { + return '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>'; + } + } + + return ''; + } + + /** + * @inheritDoc + */ + public function firstBirthName(): string + { + return $this->getFirstLastEventName(self::EVENT_BIRTH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastBirthName(): string + { + return $this->getFirstLastEventName(self::EVENT_BIRTH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDeathName(): string + { + return $this->getFirstLastEventName(self::EVENT_DEATH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDeathName(): string + { + return $this->getFirstLastEventName(self::EVENT_DEATH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstMarriageName(): string + { + return $this->getFirstLastEventName(self::EVENT_MARRIAGE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastMarriageName(): string + { + return $this->getFirstLastEventName(self::EVENT_MARRIAGE, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDivorceName(): string + { + return $this->getFirstLastEventName(self::EVENT_DIVORCE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDivorceName(): string + { + return $this->getFirstLastEventName(self::EVENT_DIVORCE, self::SORT_MAX); + } + + /** + * Returns the formatted place of the first/last occuring event. + * + * @param string $type The fact to query + * @param string $operation The sorting operation + * + * @return string + */ + private function getFirstLastEventPlace(string $type, string $operation): string + { + $row = $this->eventQuery($type, $operation); + + if ($row) { + $record = GedcomRecord::getInstance($row->id, $this->tree); + $fact = null; + + if ($record) { + $fact = $record->getFirstFact($row->fact); + } + + if ($fact) { + return FunctionsPrint::formatFactPlace($fact, true, true, true); + } + } + + return I18N::translate('Private'); + } + + /** + * @inheritDoc + */ + public function firstBirthPlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_BIRTH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastBirthPlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_BIRTH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDeathPlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_DEATH, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDeathPlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_DEATH, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstMarriagePlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_MARRIAGE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastMarriagePlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_MARRIAGE, self::SORT_MAX); + } + + /** + * @inheritDoc + */ + public function firstDivorcePlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_DIVORCE, self::SORT_MIN); + } + + /** + * @inheritDoc + */ + public function lastDivorcePlace(): string + { + return $this->getFirstLastEventPlace(self::EVENT_DIVORCE, self::SORT_MAX); + } +} diff --git a/app/Statistics/Repository/FamilyRepository.php b/app/Statistics/Repository/FamilyRepository.php new file mode 100644 index 0000000000..650dd1fe1f --- /dev/null +++ b/app/Statistics/Repository/FamilyRepository.php @@ -0,0 +1,1863 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Database; +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\Functions\FunctionsDate; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Individual; +use Fisharebest\Webtrees\Statistics\Google\ChartChildren; +use Fisharebest\Webtrees\Statistics\Google\ChartDivorce; +use Fisharebest\Webtrees\Statistics\Google\ChartFamily; +use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest; +use Fisharebest\Webtrees\Statistics\Google\ChartMarriage; +use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge; +use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies; +use Fisharebest\Webtrees\Statistics\Helper\Sql; +use Fisharebest\Webtrees\Tree; +use stdClass; + +/** + * + */ +class FamilyRepository +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * General query on family. + * + * @param string $type + * + * @return string + */ + private function familyQuery(string $type): string + { + $rows = $this->runSql( + " SELECT f_numchil AS tot, f_id AS id" . + " FROM `##families`" . + " WHERE" . + " f_file={$this->tree->id()}" . + " AND f_numchil = (" . + " SELECT max( f_numchil )" . + " FROM `##families`" . + " WHERE f_file ={$this->tree->id()}" . + " )" . + " LIMIT 1" + ); + + if (!isset($rows[0])) { + return ''; + } + + $row = $rows[0]; + $family = Family::getInstance($row->id, $this->tree); + + if (!$family) { + return ''; + } + + switch ($type) { + default: + case 'full': + if ($family->canShow()) { + $result = $family->formatList(); + } else { + $result = I18N::translate('This information is private and cannot be shown.'); + } + break; + case 'size': + $result = I18N::number((int) $row->tot); + break; + case 'name': + $result = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a>'; + break; + } + + return $result; + } + + /** + * Run an SQL query and cache the result. + * + * @param string $sql + * + * @return stdClass[] + */ + private function runSql($sql): array + { + return Sql::runSql($sql); + } + + /** + * Find the family with the most children. + * + * @return string + */ + public function largestFamily(): string + { + return $this->familyQuery('full'); + } + + /** + * Find the number of children in the largest family. + * + * @return string + */ + public function largestFamilySize(): string + { + return $this->familyQuery('size'); + } + + /** + * Find the family with the most children. + * + * @return string + */ + public function largestFamilyName(): string + { + return $this->familyQuery('name'); + } + + /** + * Find the couple with the most grandchildren. + * + * @param int $total + * + * @return array + */ + private function topTenGrandFamilyQuery(int $total): array + { + $rows = $this->runSql( + "SELECT COUNT(*) AS tot, f_id AS id" . + " FROM `##families`" . + " JOIN `##link` AS children ON children.l_file = {$this->tree->id()}" . + " JOIN `##link` AS mchildren ON mchildren.l_file = {$this->tree->id()}" . + " JOIN `##link` AS gchildren ON gchildren.l_file = {$this->tree->id()}" . + " WHERE" . + " f_file={$this->tree->id()} 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 = []; + + foreach ($rows as $row) { + $family = Family::getInstance($row->id, $this->tree); + + if ($family && $family->canShow()) { + $total = (int) $row->tot; + + $top10[] = [ + 'family' => $family, + 'count' => $total, + ]; + } + } + + // TODO +// if (I18N::direction() === 'rtl') { +// $top10 = str_replace([ +// '[', +// ']', +// '(', +// ')', +// '+', +// ], [ +// '‏[', +// '‏]', +// '‏(', +// '‏)', +// '‏+', +// ], $top10); +// } + + return $top10; + } + + /** + * Find the couple with the most grandchildren. + * + * @param int $total + * + * @return string + */ + public function topTenLargestGrandFamily(int $total = 10): string + { + $records = $this->topTenGrandFamilyQuery($total); + + return view( + 'statistics/families/top10-nolist-grand', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the couple with the most grandchildren. + * + * @param int $total + * + * @return string + */ + public function topTenLargestGrandFamilyList(int $total = 10): string + { + $records = $this->topTenGrandFamilyQuery($total); + + return view( + 'statistics/families/top10-list-grand', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the families with no children. + * + * @return int + */ + private function noChildrenFamiliesQuery(): int + { + $rows = $this->runSql( + " SELECT COUNT(*) AS tot" . + " FROM `##families`" . + " WHERE f_numchil = 0 AND f_file = {$this->tree->id()}" + ); + + return (int) $rows[0]->tot; + } + + /** + * Find the families with no children. + * + * @return string + */ + public function noChildrenFamilies(): string + { + return I18N::number($this->noChildrenFamiliesQuery()); + } + + /** + * Find the families with no children. + * + * @param string $type + * + * @return string + */ + public function noChildrenFamiliesList($type = 'list'): string + { + $rows = $this->runSql( + " SELECT f_id AS family" . + " FROM `##families` AS fam" . + " WHERE f_numchil = 0 AND fam.f_file = {$this->tree->id()}" + ); + + if (!isset($rows[0])) { + return ''; + } + + $top10 = []; + foreach ($rows as $row) { + $family = Family::getInstance($row->family, $this->tree); + if ($family->canShow()) { + if ($type === 'list') { + $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a></li>'; + } else { + $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a>'; + } + } + } + + if ($type === 'list') { + $top10 = implode('', $top10); + } else { + $top10 = implode('; ', $top10); + } + + if (I18N::direction() === 'rtl') { + $top10 = str_replace([ + '[', + ']', + '(', + ')', + '+', + ], [ + '‏[', + '‏]', + '‏(', + '‏)', + '‏+', + ], $top10); + } + if ($type === 'list') { + return '<ul>' . $top10 . '</ul>'; + } + + return $top10; + } + + /** + * Create a chart of children with no families. + * + * @param string $size + * @param int $year1 + * @param int $year2 + * + * @return string + */ + public function chartNoChildrenFamilies(string $size = '220x200', int $year1 = -1, int $year2 = -1): string + { + $no_child_fam = $this->noChildrenFamiliesQuery(); + + return (new ChartNoChildrenFamilies($this->tree)) + ->chartNoChildrenFamilies($no_child_fam, $size, $year1, $year2); + } + + /** + * Returns the ages between siblings. + * + * @param int $total The total number of records to query + * + * @return array + */ + private function ageBetweenSiblingsQuery(int $total): array + { + $rows = $this->runSql( + " SELECT 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->id()}" . + " LEFT JOIN `##dates` AS child2 ON child2.d_file = {$this->tree->id()}" . + " LEFT JOIN `##link` AS link2 ON link2.l_file = {$this->tree->id()}" . + " WHERE" . + " link1.l_file = {$this->tree->id()} 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 []; + } + + return $rows; + } + + /** + * Returns the calculated age the time of event. + * + * @param int $age The age from the database record + * + * @return string + */ + private function calculateAge(int $age): string + { + if ((int) ($age / 365.25) > 0) { + $result = (int) ($age / 365.25) . 'y'; + } elseif ((int) ($age / 30.4375) > 0) { + $result = (int) ($age / 30.4375) . 'm'; + } else { + $result = $age . 'd'; + } + + return FunctionsDate::getAgeAtEvent($result); + } + + /** + * Find the ages between siblings. + * + * @param int $total The total number of records to query + * + * @return array + * @throws \Exception + */ + private function ageBetweenSiblingsNoList(int $total): array + { + $rows = $this->ageBetweenSiblingsQuery($total); + + 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 ($child1->canShow() && $child2->canShow()) { + // ! Single array (no list) + return [ + 'child1' => $child1, + 'child2' => $child2, + 'family' => $family, + 'age' => $this->calculateAge((int) $fam->age), + ]; + } + } + + return []; + } + + /** + * Find the ages between siblings. + * + * @param int $total The total number of records to query + * @param bool $one Include each family only once if true + * + * @return array + * @throws \Exception + */ + private function ageBetweenSiblingsList(int $total, bool $one): array + { + $rows = $this->ageBetweenSiblingsQuery($total); + $top10 = []; + $dist = []; + + 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); + + $age = $this->calculateAge((int) $fam->age); + + if ($one && !\in_array($fam->family, $dist, true)) { + if ($child1->canShow() && $child2->canShow()) { + $top10[] = [ + 'child1' => $child1, + 'child2' => $child2, + 'family' => $family, + 'age' => $age, + ]; + + $dist[] = $fam->family; + } + } elseif (!$one && $child1->canShow() && $child2->canShow()) { + $top10[] = [ + 'child1' => $child1, + 'child2' => $child2, + 'family' => $family, + 'age' => $age, + ]; + } + } + + // TODO +// if (I18N::direction() === 'rtl') { +// $top10 = str_replace([ +// '[', +// ']', +// '(', +// ')', +// '+', +// ], [ +// '‏[', +// '‏]', +// '‏(', +// '‏)', +// '‏+', +// ], $top10); +// } + + return $top10; + } + + /** + * Find the ages between siblings. + * + * @param int $total The total number of records to query + * + * @return string + */ + private function ageBetweenSiblingsAge(int $total): string + { + $rows = $this->ageBetweenSiblingsQuery($total); + + foreach ($rows as $fam) { + return $this->calculateAge((int) $fam->age); + } + + return ''; + } + + /** + * Find the ages between siblings. + * + * @param int $total The total number of records to query + * + * @return string + * @throws \Exception + */ + private function ageBetweenSiblingsName(int $total): string + { + $rows = $this->ageBetweenSiblingsQuery($total); + + 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 ($child1->canShow() && $child2->canShow()) { + $return = '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> '; + $return .= I18N::translate('and') . ' '; + $return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>'; + $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; + } else { + $return = I18N::translate('This information is private and cannot be shown.'); + } + + return $return; + } + + return ''; + } + + /** + * Find the names of siblings with the widest age gap. + * + * @param int $total + * + * @return string + */ + public function topAgeBetweenSiblingsName(int $total = 10): string + { + return $this->ageBetweenSiblingsName($total); + } + + /** + * Find the widest age gap between siblings. + * + * @param int $total + * + * @return string + */ + public function topAgeBetweenSiblings(int $total = 10): string + { + return $this->ageBetweenSiblingsAge($total); + } + + /** + * Find the name of siblings with the widest age gap. + * + * @param int $total + * + * @return string + */ + public function topAgeBetweenSiblingsFullName(int $total = 10): string + { + $record = $this->ageBetweenSiblingsNoList($total); + + return view( + 'statistics/families/top10-nolist-age', + [ + 'record' => $record, + ] + ); + } + + /** + * Find the siblings with the widest age gaps. + * + * @param int $total + * @param string $one + * + * @return string + */ + public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string + { + $records = $this->ageBetweenSiblingsList($total, (bool) $one); + + return view( + 'statistics/families/top10-list-age', + [ + 'records' => $records, + ] + ); + } + + /** + * General query on familes/children. + * + * @param string $sex + * @param int $year1 + * @param int $year2 + * + * @return stdClass[] + */ + public function statsChildrenQuery(string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array + { + if ($sex === 'M') { + $sql = + "SELECT 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->id()} GROUP BY l_to" . + ") boys" . + " GROUP BY num" . + " ORDER BY num"; + } elseif ($sex === 'F') { + $sql = + "SELECT 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->id()} GROUP BY l_to" . + ") girls" . + " GROUP BY num" . + " ORDER BY num"; + } else { + $sql = "SELECT 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->id()}" + . " WHERE" + . " married.d_gid = fam.f_id AND" + . " fam.f_file = {$this->tree->id()} AND" + . " married.d_fact = 'MARR' AND" + . " married.d_year BETWEEN '{$year1}' AND '{$year2}'"; + } else { + $sql .= "WHERE f_file={$this->tree->id()}"; + } + + $sql .= ' GROUP BY f_numchil'; + } + + return $this->runSql($sql); + } + + /** + * Genearl query on families/children. + * + * @param string $size + * + * @return string + */ + public function statsChildren(string $size = '220x200'): string + { + return (new ChartChildren($this->tree)) + ->chartChildren($size); + } + + /** + * Count the total children. + * + * @return string + */ + public function totalChildren(): string + { + $total = (int) Database::prepare( + "SELECT SUM(f_numchil) FROM `##families` WHERE f_file = :tree_id" + )->execute([ + 'tree_id' => $this->tree->id(), + ])->fetchOne(); + + return I18N::number($total); + } + + /** + * Find the average number of children in families. + * + * @return string + */ + public function averageChildren(): string + { + $average = (float) Database::prepare( + "SELECT AVG(f_numchil) AS tot FROM `##families` WHERE f_file = :tree_id" + )->execute([ + 'tree_id' => $this->tree->id(), + ])->fetchOne(); + + return I18N::number($average, 2); + } + + /** + * General query on families. + * + * @param int $total + * + * @return array + */ + private function topTenFamilyQuery(int $total): array + { + $rows = $this->runSql( + "SELECT f_numchil AS tot, f_id AS id" . + " FROM `##families`" . + " WHERE" . + " f_file={$this->tree->id()}" . + " ORDER BY tot DESC" . + " LIMIT " . $total + ); + + if (empty($rows)) { + return []; + } + + $top10 = []; + foreach ($rows as $row) { + $family = Family::getInstance($row->id, $this->tree); + + if ($family && $family->canShow()) { + $top10[] = [ + 'family' => $family, + 'count' => (int) $row->tot, + ]; + } + } + + // TODO +// if (I18N::direction() === 'rtl') { +// $top10 = str_replace([ +// '[', +// ']', +// '(', +// ')', +// '+', +// ], [ +// '‏[', +// '‏]', +// '‏(', +// '‏)', +// '‏+', +// ], $top10); +// } + + return $top10; + } + + /** + * The the families with the most children. + * + * @param int $total + * + * @return string + */ + public function topTenLargestFamily(int $total = 10): string + { + $records = $this->topTenFamilyQuery($total); + + return view( + 'statistics/families/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the families with the most children. + * + * @param int $total + * + * @return string + */ + public function topTenLargestFamilyList(int $total = 10): string + { + $records = $this->topTenFamilyQuery($total); + + return view( + 'statistics/families/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Create a chart of the largest families. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * @param int $total + * + * @return string + */ + public function chartLargestFamilies( + string $size = null, + string $color_from = null, + string $color_to = null, + int $total = 10 + ): string { + return (new ChartFamilyLargest($this->tree)) + ->chartLargestFamilies($size, $color_from, $color_to, $total); + } + + /** + * Find the month in the year of the birth of the first child. + * + * @param bool $sex + * + * @return stdClass[] + */ + public function monthFirstChildQuery(bool $sex = false): array + { + 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 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->id()}" . + $sql_sex2 . + " WHERE" . + " link1.l_file = {$this->tree->id()} AND" . + " link1.l_type = 'CHIL' AND" . + " child1.d_gid = link1.l_to AND" . + " child1.d_fact = 'BIRT' AND" . + " child1.d_month IN ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')" . + " ORDER BY date" . + " ) AS children" . + " GROUP BY family, d_month{$sql_sex1}" . + ") AS first_child " . + "GROUP BY d_month"; + + if ($sex) { + $sql .= ', i_sex'; + } + + return $this->runSql($sql); + } + + /** + * Number of husbands. + * + * @return string + */ + public function totalMarriedMales(): string + { + $n = (int) Database::prepare( + "SELECT COUNT(DISTINCT f_husb) FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\\n1 MARR%'" + )->execute([ + 'tree_id' => $this->tree->id(), + ])->fetchOne(); + + return I18N::number($n); + } + + /** + * Number of wives. + * + * @return string + */ + public function totalMarriedFemales(): string + { + $n = (int) Database::prepare( + "SELECT COUNT(DISTINCT f_wife) FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\\n1 MARR%'" + )->execute([ + 'tree_id' => $this->tree->id(), + ])->fetchOne(); + + return I18N::number($n); + } + + /** + * General query on parents. + * + * @param string $type + * @param string $age_dir + * @param string $sex + * @param bool $show_years + * + * @return string + */ + private function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string + { + if ($sex === 'F') { + $sex_field = 'WIFE'; + } else { + $sex_field = 'HUSB'; + } + + if ($age_dir !== 'ASC') { + $age_dir = 'DESC'; + } + + $rows = $this->runSql( + " SELECT" . + " 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->id()}" . + " JOIN `##dates` AS birth ON birth.d_file = {$this->tree->id()}" . + " JOIN `##dates` AS childbirth ON childbirth.d_file = {$this->tree->id()}" . + " 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->id()} 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 && $person->canShow()) { + $result = $person->formatList(); + } else { + $result = I18N::translate('This information is private and cannot be shown.'); + } + break; + + case 'name': + $result = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>'; + break; + + case 'age': + $age = $row->age; + + if ($show_years) { + $result = $this->calculateAge((int) $row->age); + } else { + $result = (string) floor($age / 365.25); + } + + break; + } + + return $result; + } + + /** + * Find the youngest mother + * + * @return string + */ + public function youngestMother(): string + { + return $this->parentsQuery('full', 'ASC', 'F', false); + } + + /** + * Find the name of the youngest mother. + * + * @return string + */ + public function youngestMotherName(): string + { + return $this->parentsQuery('name', 'ASC', 'F', false); + } + + /** + * Find the age of the youngest mother. + * + * @param string $show_years + * + * @return string + */ + public function youngestMotherAge(string $show_years = ''): string + { + return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years); + } + + /** + * Find the oldest mother. + * + * @return string + */ + public function oldestMother(): string + { + return $this->parentsQuery('full', 'DESC', 'F', false); + } + + /** + * Find the name of the oldest mother. + * + * @return string + */ + public function oldestMotherName(): string + { + return $this->parentsQuery('name', 'DESC', 'F', false); + } + + /** + * Find the age of the oldest mother. + * + * @param string $show_years + * + * @return string + */ + public function oldestMotherAge(string $show_years = ''): string + { + return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years); + } + + /** + * Find the youngest father. + * + * @return string + */ + public function youngestFather(): string + { + return $this->parentsQuery('full', 'ASC', 'M', false); + } + + /** + * Find the name of the youngest father. + * + * @return string + */ + public function youngestFatherName(): string + { + return $this->parentsQuery('name', 'ASC', 'M', false); + } + + /** + * Find the age of the youngest father. + * + * @param string $show_years + * + * @return string + */ + public function youngestFatherAge(string $show_years = ''): string + { + return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years); + } + + /** + * Find the oldest father. + * + * @return string + */ + public function oldestFather(): string + { + return $this->parentsQuery('full', 'DESC', 'M', false); + } + + /** + * Find the name of the oldest father. + * + * @return string + */ + public function oldestFatherName(): string + { + return $this->parentsQuery('name', 'DESC', 'M', false); + } + + /** + * Find the age of the oldest father. + * + * @param string $show_years + * + * @return string + */ + public function oldestFatherAge(string $show_years = ''): string + { + return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years); + } + + /** + * General query on age at marriage. + * + * @param string $type + * @param string $age_dir + * @param int $total + * + * @return string + */ + private function ageOfMarriageQuery(string $type, string $age_dir, int $total): string + { + if ($age_dir !== 'ASC') { + $age_dir = 'DESC'; + } + + $hrows = $this->runSql( + " SELECT 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->id()}" . + " LEFT JOIN `##dates` AS husbdeath ON husbdeath.d_file = {$this->tree->id()}" . + " WHERE" . + " fam.f_file = {$this->tree->id()} 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 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->id()}" . + " LEFT JOIN `##dates` AS wifedeath ON wifedeath.d_file = {$this->tree->id()}" . + " WHERE" . + " fam.f_file = {$this->tree->id()} 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 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->id()}" . + " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->id()}" . + " WHERE" . + " fam.f_file = {$this->tree->id()} 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}" + ); + + $rows = []; + 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 = []; + $i = 0; + foreach ($rows as $fam => $age) { + $family = Family::getInstance($fam, $this->tree); + if ($type === 'name') { + return $family->formatList(); + } + + $age = $this->calculateAge((int) $age); + + if ($type === 'age') { + return $age; + } + + $husb = $family->getHusband(); + $wife = $family->getWife(); + + if (($husb && ($husb->getAllDeathDates() || !$husb->isDead())) + && ($wife && ($wife->getAllDeathDates() || !$wife->isDead())) + ) { + if ($family && $family->canShow()) { + if ($type === 'list') { + $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')' . '</li>'; + } else { + $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')'; + } + } + if (++$i === $total) { + break; + } + } + } + + if ($type === 'list') { + $top10 = implode('', $top10); + } else { + $top10 = implode('; ', $top10); + } + + if (I18N::direction() === 'rtl') { + $top10 = str_replace([ + '[', + ']', + '(', + ')', + '+', + ], [ + '‏[', + '‏]', + '‏(', + '‏)', + '‏+', + ], $top10); + } + + if ($type === 'list') { + return '<ul>' . $top10 . '</ul>'; + } + + return $top10; + } + + /** + * General query on marriage ages. + * + * @return string + */ + public function topAgeOfMarriageFamily(): string + { + return $this->ageOfMarriageQuery('name', 'DESC', 1); + } + + /** + * General query on marriage ages. + * + * @return string + */ + public function topAgeOfMarriage(): string + { + return $this->ageOfMarriageQuery('age', 'DESC', 1); + } + + /** + * General query on marriage ages. + * + * @param int $total + * + * @return string + */ + public function topAgeOfMarriageFamilies(int $total = 10): string + { + return $this->ageOfMarriageQuery('nolist', 'DESC', $total); + } + + /** + * General query on marriage ages. + * + * @param int $total + * + * @return string + */ + public function topAgeOfMarriageFamiliesList(int $total = 10): string + { + return $this->ageOfMarriageQuery('list', 'DESC', $total); + } + + /** + * General query on marriage ages. + * + * @return string + */ + public function minAgeOfMarriageFamily(): string + { + return $this->ageOfMarriageQuery('name', 'ASC', 1); + } + + /** + * General query on marriage ages. + * + * @return string + */ + public function minAgeOfMarriage(): string + { + return $this->ageOfMarriageQuery('age', 'ASC', 1); + } + + /** + * General query on marriage ages. + * + * @param int $total + * + * @return string + */ + public function minAgeOfMarriageFamilies(int $total = 10): string + { + return $this->ageOfMarriageQuery('nolist', 'ASC', $total); + } + + /** + * General query on marriage ages. + * + * @param int $total + * + * @return string + */ + public function minAgeOfMarriageFamiliesList(int $total = 10): string + { + return $this->ageOfMarriageQuery('list', 'ASC', $total); + } + + /** + * Find the ages between spouses. + * + * @param string $age_dir + * @param int $total + * + * @return array + */ + private function ageBetweenSpousesQuery(string $age_dir, int $total): array + { + if ($age_dir === 'DESC') { + $sql = + "SELECT 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 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([ + 'tree_id' => $this->tree->id(), + 'limit' => $total, + ])->fetchAll(); + + $top10 = []; + + foreach ($rows as $fam) { + $family = Family::getInstance($fam->xref, $this->tree); + + if ($fam->age < 0) { + break; + } + + if ($family->canShow()) { + $top10[] = [ + 'family' => $family, + 'age' => $this->calculateAge((int) $fam->age), + ]; + } + } + + return $top10; + } + + /** + * Find the age between husband and wife. + * + * @param int $total + * + * @return string + */ + public function ageBetweenSpousesMF(int $total = 10): string + { + $records = $this->ageBetweenSpousesQuery('DESC', $total); + + return view( + 'statistics/families/top10-nolist-spouses', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the age between husband and wife. + * + * @param int $total + * + * @return string + */ + public function ageBetweenSpousesMFList(int $total = 10): string + { + $records = $this->ageBetweenSpousesQuery('DESC', $total); + + return view( + 'statistics/families/top10-list-spouses', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the age between wife and husband.. + * + * @param int $total + * + * @return string + */ + public function ageBetweenSpousesFM(int $total = 10): string + { + $records = $this->ageBetweenSpousesQuery('ASC', $total); + + return view( + 'statistics/families/top10-nolist-spouses', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the age between wife and husband.. + * + * @param int $total + * + * @return string + */ + public function ageBetweenSpousesFMList(int $total = 10): string + { + $records = $this->ageBetweenSpousesQuery('ASC', $total); + + return view( + 'statistics/families/top10-list-spouses', + [ + 'records' => $records, + ] + ); + } + + /** + * General query on ages at marriage. + * + * @param string $sex + * @param int $year1 + * @param int $year2 + * + * @return array + */ + public function statsMarrAgeQuery($sex = 'M', $year1 = -1, $year2 = -1): array + { + if ($year1 >= 0 && $year2 >= 0) { + $years = " married.d_year BETWEEN {$year1} AND {$year2} AND "; + } else { + $years = ''; + } + + $rows = $this->runSql( + "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_husb AND birth.d_file=fam.f_file) " . + "WHERE " . + " '{$sex}' IN ('M', 'BOTH') AND {$years} " . + " married.d_file={$this->tree->id()} 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->id()} 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 " + ); + + foreach ($rows as $row) { + $row->age = (int) $row->age; + } + + return $rows; + } + + /** + * General query on marriage ages. + * + * @param string $size + * + * @return string + */ + public function statsMarrAge(string $size = '200x250'): string + { + return (new ChartMarriageAge($this->tree)) + ->chartMarriageAge($size); + } + + /** + * 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(string $type, string $age_dir, string $sex, bool $show_years): string + { + if ($sex === 'F') { + $sex_field = 'f_wife'; + } else { + $sex_field = 'f_husb'; + } + + if ($age_dir !== 'ASC') { + $age_dir = 'DESC'; + } + + $rows = $this->runSql( + " SELECT 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->id()}" . + " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . + " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . + " 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->id()} 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 && $family->canShow()) { + $result = $family->formatList(); + } else { + $result = I18N::translate('This information is private and cannot be shown.'); + } + break; + + case 'name': + $result = '<a href="' . e($family->url()) . '">' . $person->getFullName() . '</a>'; + break; + + case 'age': + $age = $row->age; + + if ($show_years) { + $result = $this->calculateAge((int) $row->age); + } else { + $result = I18N::number((int) ($age / 365.25)); + } + + break; + } + + return $result; + } + + /** + * Find the youngest wife. + * + * @return string + */ + public function youngestMarriageFemale(): string + { + return $this->marriageQuery('full', 'ASC', 'F', false); + } + + /** + * Find the name of the youngest wife. + * + * @return string + */ + public function youngestMarriageFemaleName(): string + { + return $this->marriageQuery('name', 'ASC', 'F', false); + } + + /** + * Find the age of the youngest wife. + * + * @param string $show_years + * + * @return string + */ + public function youngestMarriageFemaleAge(string $show_years = ''): string + { + return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years); + } + + /** + * Find the oldest wife. + * + * @return string + */ + public function oldestMarriageFemale(): string + { + return $this->marriageQuery('full', 'DESC', 'F', false); + } + + /** + * Find the name of the oldest wife. + * + * @return string + */ + public function oldestMarriageFemaleName(): string + { + return $this->marriageQuery('name', 'DESC', 'F', false); + } + + /** + * Find the age of the oldest wife. + * + * @param string $show_years + * + * @return string + */ + public function oldestMarriageFemaleAge(string $show_years = ''): string + { + return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years); + } + + /** + * Find the youngest husband. + * + * @return string + */ + public function youngestMarriageMale(): string + { + return $this->marriageQuery('full', 'ASC', 'M', false); + } + + /** + * Find the name of the youngest husband. + * + * @return string + */ + public function youngestMarriageMaleName(): string + { + return $this->marriageQuery('name', 'ASC', 'M', false); + } + + /** + * Find the age of the youngest husband. + * + * @param string $show_years + * + * @return string + */ + public function youngestMarriageMaleAge(string $show_years = ''): string + { + return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years); + } + + /** + * Find the oldest husband. + * + * @return string + */ + public function oldestMarriageMale(): string + { + return $this->marriageQuery('full', 'DESC', 'M', false); + } + + /** + * Find the name of the oldest husband. + * + * @return string + */ + public function oldestMarriageMaleName(): string + { + return $this->marriageQuery('name', 'DESC', 'M', false); + } + + /** + * Find the age of the oldest husband. + * + * @param string $show_years + * + * @return string + */ + public function oldestMarriageMaleAge(string $show_years = ''): string + { + return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years); + } + + /** + * General query on marriages. + * + * @param bool $first + * @param int $year1 + * @param int $year2 + * + * @return array + */ + public function statsMarrQuery(bool $first = false, int $year1 = -1, int $year2 = -1): array + { + if ($first) { + $years = ''; + + if ($year1 >= 0 && $year2 >= 0) { + $years = " married.d_year BETWEEN '{$year1}' AND '{$year2}' AND"; + } + + $sql = + " SELECT 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->id()}" . + " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . + " WHERE" . + " married.d_gid = fam.f_id AND" . + " fam.f_file = {$this->tree->id()} 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 d_month, COUNT(*) AS total" . + " FROM `##dates`" . + " WHERE d_file={$this->tree->id()} AND d_fact='MARR'"; + + if ($year1 >= 0 && $year2 >= 0) { + $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; + } + + $sql .= " GROUP BY d_month"; + } + + return $this->runSql($sql); + } + + /** + * General query on marriages. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function statsMarr(string $size = null, string $color_from = null, string $color_to = null): string + { + return (new ChartMarriage($this->tree)) + ->chartMarriage($size, $color_from, $color_to); + } + + /** + * General divorce query. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function statsDiv(string $size = null, string $color_from = null, string $color_to = null): string + { + return (new ChartDivorce($this->tree)) + ->chartDivorce($size, $color_from, $color_to); + } +} diff --git a/app/Statistics/Repository/FavoritesRepository.php b/app/Statistics/Repository/FavoritesRepository.php new file mode 100644 index 0000000000..e061742507 --- /dev/null +++ b/app/Statistics/Repository/FavoritesRepository.php @@ -0,0 +1,105 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Module; +use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule; +use Fisharebest\Webtrees\Module\UserFavoritesModule; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\FavoritesRepositoryInterface; +use Fisharebest\Webtrees\Tree; + +/** + * A repository providing methods for favorites related statistics. + */ +class FavoritesRepository implements FavoritesRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * @inheritDoc + */ + public function gedcomFavorites(): string + { + $module = Module::findByClass(FamilyTreeFavoritesModule::class); + + if ($module instanceof FamilyTreeFavoritesModule) { + return $module->getBlock($this->tree, 0); + } + + return ''; + } + + /** + * @inheritDoc + */ + public function userFavorites(): string + { + $module = Module::findByClass(UserFavoritesModule::class); + + if ($module instanceof UserFavoritesModule) { + return $module->getBlock($this->tree, 0); + } + + return ''; + } + + /** + * @inheritDoc + */ + public function totalGedcomFavorites(): string + { + $count = 0; + $module = Module::findByClass(FamilyTreeFavoritesModule::class); + + if ($module instanceof FamilyTreeFavoritesModule) { + $count = \count($module->getFavorites($this->tree)); + } + + return I18N::number($count); + } + + /** + * @inheritDoc + */ + public function totalUserFavorites(): string + { + $count = 0; + $module = Module::findByClass(UserFavoritesModule::class); + + if ($module instanceof UserFavoritesModule) { + $count = \count($module->getFavorites($this->tree, Auth::user())); + } + + return I18N::number($count); + } +} diff --git a/app/Statistics/Repository/GedcomRepository.php b/app/Statistics/Repository/GedcomRepository.php new file mode 100644 index 0000000000..433b663e48 --- /dev/null +++ b/app/Statistics/Repository/GedcomRepository.php @@ -0,0 +1,183 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Date; +use Fisharebest\Webtrees\GedcomRecord; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\GedcomRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\Builder; + +/** + * A repository providing methods for GEDCOM related statistics. + */ +class GedcomRepository implements GedcomRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Get information from the GEDCOM's HEAD record. + * + * @return string[] + */ + private function gedcomHead(): array + { + $title = ''; + $version = ''; + $source = ''; + + $head = GedcomRecord::getInstance('HEAD', $this->tree); + + if ($head !== null) { + $sour = $head->getFirstFact('SOUR'); + + if ($sour !== null) { + $source = $sour->value(); + $title = $sour->attribute('NAME'); + $version = $sour->attribute('VERS'); + } + } + + return [ + $title, + $version, + $source, + ]; + } + + /** + * @inheritDoc + */ + public function gedcomFilename(): string + { + return $this->tree->name(); + } + + /** + * @inheritDoc + */ + public function gedcomId(): int + { + return $this->tree->id(); + } + + /** + * @inheritDoc + */ + public function gedcomTitle(): string + { + return e($this->tree->title()); + } + + /** + * @inheritDoc + */ + public function gedcomCreatedSoftware(): string + { + return $this->gedcomHead()[0]; + } + + /** + * @inheritDoc + */ + public function gedcomCreatedVersion(): string + { + $head = $this->gedcomHead(); + + if ($head === null) { + return ''; + } + + // Fix broken version string in Family Tree Maker + if (strpos($head[1], 'Family Tree Maker ') !== false) { + $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]; + } + + /** + * @inheritDoc + */ + public function gedcomDate(): string + { + $head = GedcomRecord::getInstance('HEAD', $this->tree); + + if ($head !== null) { + $fact = $head->getFirstFact('DATE'); + + if ($fact) { + return (new Date($fact->value()))->display(); + } + } + + return ''; + } + + /** + * @inheritDoc + */ + public function gedcomUpdated(): string + { + $row = DB::table('dates') + ->select(['d_year', 'd_month', 'd_day']) + ->where('d_julianday1', '=', function (Builder $query) { + $query->selectRaw('MAX(d_julianday1)') + ->from('dates') + ->where('d_file', '=', $this->tree->id()) + ->where('d_fact', '=', 'CHAN'); + }) + ->first(); + + if ($row) { + $date = new Date("{$row->d_day} {$row->d_month} {$row->d_year}"); + return $date->display(); + } + + return $this->gedcomDate(); + } + + /** + * @inheritDoc + */ + public function gedcomRootId(): string + { + return $this->tree->getPreference('PEDIGREE_ROOT_ID'); + } +} diff --git a/app/Statistics/Repository/HitCountRepository.php b/app/Statistics/Repository/HitCountRepository.php new file mode 100644 index 0000000000..98d5681450 --- /dev/null +++ b/app/Statistics/Repository/HitCountRepository.php @@ -0,0 +1,143 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\HitCountRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Fisharebest\Webtrees\User; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * A repository providing methods for hit count related statistics. + */ +class HitCountRepository implements HitCountRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * These functions provide access to hitcounter for use in the HTML block. + * + * @param string $page_name + * @param string $page_parameter + * + * @return string + */ + private function hitCountQuery($page_name, string $page_parameter = ''): string + { + if ($page_name === '') { + // index.php?ctype=gedcom + $page_name = 'index.php'; + $page_parameter = 'gedcom:' . $this->tree->id(); + } elseif ($page_name === 'index.php') { + // index.php?ctype=user + $user = User::findByIdentifier($page_parameter); + $page_parameter = 'user:' . ($user ? $user->id() : Auth::id()); + } + + $count = (int) DB::table('hit_counter') + ->where('gedcom_id', '=', $this->tree->id()) + ->where('page_name', '=', $page_name) + ->where('page_parameter', '=', $page_parameter) + ->value('page_count'); + + return view( + 'statistics/hit-count', + [ + 'count' => $count, + ] + ); + } + + /** + * @inheritDoc + */ + public function hitCount(string $page_parameter = ''): string + { + return $this->hitCountQuery('', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountUser(string $page_parameter = ''): string + { + return $this->hitCountQuery('index.php', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountIndi(string $page_parameter = ''): string + { + return $this->hitCountQuery('individual.php', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountFam(string $page_parameter = ''): string + { + return $this->hitCountQuery('family.php', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountSour(string $page_parameter = ''): string + { + return $this->hitCountQuery('source.php', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountRepo(string $page_parameter = ''): string + { + return $this->hitCountQuery('repo.php', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountNote(string $page_parameter = ''): string + { + return $this->hitCountQuery('note.php', $page_parameter); + } + + /** + * @inheritDoc + */ + public function hitCountObje(string $page_parameter = ''): string + { + return $this->hitCountQuery('mediaviewer.php', $page_parameter); + } +} diff --git a/app/Statistics/Repository/IndividualRepository.php b/app/Statistics/Repository/IndividualRepository.php new file mode 100644 index 0000000000..291d7e3384 --- /dev/null +++ b/app/Statistics/Repository/IndividualRepository.php @@ -0,0 +1,2027 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\Database; +use Fisharebest\Webtrees\Functions\FunctionsDate; +use Fisharebest\Webtrees\Functions\FunctionsPrintLists; +use Fisharebest\Webtrees\Gedcom; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Individual; +use Fisharebest\Webtrees\Statistics\Google\ChartAge; +use Fisharebest\Webtrees\Statistics\Google\ChartBirth; +use Fisharebest\Webtrees\Statistics\Google\ChartCommonGiven; +use Fisharebest\Webtrees\Statistics\Google\ChartCommonSurname; +use Fisharebest\Webtrees\Statistics\Google\ChartDeath; +use Fisharebest\Webtrees\Statistics\Google\ChartFamily; +use Fisharebest\Webtrees\Statistics\Google\ChartFamilyWithSources; +use Fisharebest\Webtrees\Statistics\Google\ChartIndividual; +use Fisharebest\Webtrees\Statistics\Google\ChartMortality; +use Fisharebest\Webtrees\Statistics\Google\ChartSex; +use Fisharebest\Webtrees\Statistics\Helper\Sql; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\JoinClause; + +/** + * + */ +class IndividualRepository implements IndividualRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Run an SQL query and cache the result. + * + * @param string $sql + * + * @return \stdClass[] + */ + private function runSql(string $sql): array + { + return Sql::runSql($sql); + } + + /** + * Find common given names. + * + * @param string $sex + * @param string $type + * @param bool $show_tot + * @param int $threshold + * @param int $maxtoshow + * + * @return string|int[] + */ + private function commonGivenQuery(string $sex, string $type, bool $show_tot, int $threshold, int $maxtoshow) + { + 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->id(); + + $rows = Database::prepare("SELECT 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 = []; + foreach ($rows as $row) { + $row->num = (int) $row->num; + + // 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] += (int) $row->num; + } else { + $nameList[$given] = (int) $row->num; + } + } + } + } + arsort($nameList); + $nameList = \array_slice($nameList, 0, $maxtoshow); + + foreach ($nameList as $given => $total) { + if ($total < $threshold) { + unset($nameList[$given]); + } + } + + switch ($type) { + case 'chart': + return $nameList; + + case 'table': + return view('lists/given-names-table', [ + 'given_names' => $nameList, + ]); + + case 'list': + return view('lists/given-names-list', [ + 'given_names' => $nameList, + 'show_totals' => $show_tot, + ]); + + case 'nolist': + default: + array_walk($nameList, function (int &$value, string $key) use ($show_tot): void { + if ($show_tot) { + $value = '<span dir="auto">' . e($key); + } else { + $value = '<span dir="auto">' . e($key) . ' (' . I18N::number($value) . ')'; + } + }); + + return implode(I18N::$list_separator, $nameList); + } + } + + /** + * Find common give names. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGiven(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('B', 'nolist', false, $threshold, $maxtoshow); + } + + /** + * Find common give names. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('B', 'nolist', true, $threshold, $maxtoshow); + } + + /** + * Find common give names. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenList(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('B', 'list', false, $threshold, $maxtoshow); + } + + /** + * Find common give names. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenListTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('B', 'list', true, $threshold, $maxtoshow); + } + + /** + * Find common give names. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenTable(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('B', 'table', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of females. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenFemale(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('F', 'nolist', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of females. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenFemaleTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('F', 'nolist', true, $threshold, $maxtoshow); + } + + /** + * Find common give names of females. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenFemaleList(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('F', 'list', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of females. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenFemaleListTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('F', 'list', true, $threshold, $maxtoshow); + } + + /** + * Find common give names of females. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenFemaleTable(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('F', 'table', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of males. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenMale(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('M', 'nolist', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of males. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenMaleTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('M', 'nolist', true, $threshold, $maxtoshow); + } + + /** + * Find common give names of males. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenMaleList(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('M', 'list', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of males. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenMaleListTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('M', 'list', true, $threshold, $maxtoshow); + } + + /** + * Find common give names of males. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenMaleTable(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('M', 'table', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of unknown sexes. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenUnknown(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('U', 'nolist', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of unknown sexes. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenUnknownTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('U', 'nolist', true, $threshold, $maxtoshow); + } + + /** + * Find common give names of unknown sexes. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenUnknownList(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('U', 'list', false, $threshold, $maxtoshow); + } + + /** + * Find common give names of unknown sexes. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenUnknownListTotals(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('U', 'list', true, $threshold, $maxtoshow); + } + + /** + * Find common give names of unknown sexes. + * + * @param int $threshold + * @param int $maxtoshow + * + * @return string + */ + public function commonGivenUnknownTable(int $threshold = 1, int $maxtoshow = 10): string + { + return $this->commonGivenQuery('U', 'table', false, $threshold, $maxtoshow); + } + + /** + * Count the number of distinct given names, or count the number of + * occurrences of a specific name or names. + * + * @param array ...$params + * + * @return string + */ + public function totalGivennames(...$params): string + { + if ($params) { + $qs = implode(',', array_fill(0, \count($params), '?')); + $params[] = $this->tree->id(); + $total = (int) Database::prepare( + "SELECT COUNT( n_givn) FROM `##name` WHERE n_givn IN ({$qs}) AND n_file=?" + )->execute( + $params + )->fetchOne(); + } else { + $total = (int) Database::prepare( + "SELECT COUNT(DISTINCT n_givn) FROM `##name` WHERE n_givn IS NOT NULL AND n_file=?" + )->execute([ + $this->tree->id(), + ])->fetchOne(); + } + + return I18N::number($total); + } + + /** + * Count the surnames. + * + * @param array ...$params + * + * @return string + */ + public function totalSurnames(...$params): string + { + if ($params) { + $opt = 'IN (' . implode(',', array_fill(0, \count($params), '?')) . ')'; + $distinct = ''; + } else { + $opt = "IS NOT NULL"; + $distinct = 'DISTINCT'; + } + $params[] = $this->tree->id(); + + $total = (int) Database::prepare( + "SELECT 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); + } + + /** + * @param int $number_of_surnames + * @param int $threshold + * + * @return \stdClass[] + */ + private function topSurnames(int $number_of_surnames, int $threshold): array + { + // Use the count of base surnames. + $top_surnames = Database::prepare( + "SELECT n_surn FROM `##name`" . + " WHERE n_file = :tree_id AND n_type != '_MARNM' AND n_surn NOT IN ('@N.N.', '')" . + " GROUP BY n_surn" . + " ORDER BY COUNT(n_surn) DESC" . + " LIMIT :limit" + )->execute([ + 'tree_id' => $this->tree->id(), + 'limit' => $number_of_surnames, + ])->fetchOneColumn(); + + $surnames = []; + foreach ($top_surnames as $top_surname) { + $variants = Database::prepare( + "SELECT n_surname COLLATE utf8_bin, COUNT(*) FROM `##name` WHERE n_file = :tree_id AND n_surn COLLATE :collate = :surname GROUP BY 1" + )->execute([ + 'collate' => I18N::collation(), + 'surname' => $top_surname, + 'tree_id' => $this->tree->id(), + ])->fetchAssoc(); + + if (array_sum($variants) > $threshold) { + $surnames[$top_surname] = $variants; + } + } + + return $surnames; + } + + /** + * Find common surnames. + * + * @return string + */ + public function getCommonSurname(): string + { + $top_surname = $this->topSurnames(1, 0); + return implode(', ', array_keys(array_shift($top_surname)) ?? []); + } + + /** + * Find common surnames. + * + * @param string $type + * @param bool $show_tot + * @param int $threshold + * @param int $number_of_surnames + * @param string $sorting + * + * @return string + */ + private function commonSurnamesQuery( + string $type, + bool $show_tot, + int $threshold, + int $number_of_surnames, + string $sorting + ): string { + $surnames = $this->topSurnames($number_of_surnames, $threshold); + + switch ($sorting) { + default: + case 'alpha': + uksort($surnames, [I18N::class, 'strcasecmp']); + break; + case 'count': + break; + case 'rcount': + $surnames = array_reverse($surnames, true); + break; + } + + return FunctionsPrintLists::surnameList( + $surnames, + ($type === 'list' ? 1 : 2), + $show_tot, + 'individual-list', + $this->tree + ); + } + + /** + * Find common surnames. + * + * @param int $threshold + * @param int $number_of_surnames + * @param string $sorting + * + * @return string + */ + public function commonSurnames( + int $threshold = 1, + int $number_of_surnames = 10, + string $sorting = 'alpha' + ): string { + return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting); + } + + /** + * Find common surnames. + * + * @param int $threshold + * @param int $number_of_surnames + * @param string $sorting + * + * @return string + */ + public function commonSurnamesTotals( + int $threshold = 1, + int $number_of_surnames = 10, + string $sorting = 'rcount' + ): string { + return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting); + } + + /** + * Find common surnames. + * + * @param int $threshold + * @param int $number_of_surnames + * @param string $sorting + * + * @return string + */ + public function commonSurnamesList( + int $threshold = 1, + int $number_of_surnames = 10, + string $sorting = 'alpha' + ): string { + return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting); + } + + /** + * Find common surnames. + * + * @param int $threshold + * @param int $number_of_surnames + * @param string $sorting + * + * @return string + */ + public function commonSurnamesListTotals( + int $threshold = 1, + int $number_of_surnames = 10, + string $sorting = 'rcount' + ): string { + return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting); + } + + /** + * Get a list of birth dates. + * + * @param bool $sex + * @param int $year1 + * @param int $year2 + * + * @return array + */ + public function statsBirthQuery(bool $sex = false, int $year1 = -1, int $year2 = -1): array + { + if ($sex) { + $sql = + "SELECT 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->id()} AND " . + "d_fact='BIRT' AND " . + "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; + } else { + $sql = + "SELECT d_month, COUNT(*) AS total FROM `##dates` " . + "WHERE " . + "d_file={$this->tree->id()} AND " . + "d_fact='BIRT' AND " . + "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; + } + + if ($year1 >= 0 && $year2 >= 0) { + $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; + } + + $sql .= " GROUP BY d_month"; + + if ($sex) { + $sql .= ", i_sex"; + } + + return $this->runSql($sql); + } + + /** + * General query on births. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function statsBirth(string $size = null, string $color_from = null, string $color_to = null): string + { + return (new ChartBirth($this->tree)) + ->chartBirth($size, $color_from, $color_to); + } + + /** + * Get a list of death dates. + * + * @param bool $sex + * @param int $year1 + * @param int $year2 + * + * @return array + */ + public function statsDeathQuery(bool $sex = false, int $year1 = -1, int $year2 = -1): array + { + if ($sex) { + $sql = + "SELECT 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->id()} AND " . + "d_fact='DEAT' AND " . + "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; + } else { + $sql = + "SELECT d_month, COUNT(*) AS total FROM `##dates` " . + "WHERE " . + "d_file={$this->tree->id()} AND " . + "d_fact='DEAT' AND " . + "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; + } + + if ($year1 >= 0 && $year2 >= 0) { + $sql .= " AND d_year BETWEEN '{$year1}' AND '{$year2}'"; + } + + $sql .= " GROUP BY d_month"; + + if ($sex) { + $sql .= ", i_sex"; + } + + return $this->runSql($sql); + } + + /** + * General query on deaths. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function statsDeath(string $size = null, string $color_from = null, string $color_to = null): string + { + return (new ChartDeath($this->tree)) + ->chartDeath($size, $color_from, $color_to); + } + + /** + * General query on ages. + * + * @param string $related + * @param string $sex + * @param int $year1 + * @param int $year2 + * + * @return array|string + */ + public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1) + { + $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" . + " 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->id()} 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 $size + * + * @return string + */ + public function statsAge(string $size = '230x250'): string + { + return (new ChartAge($this->tree))->chartAge($size); + } + + /** + * Lifespan + * + * @param string $type + * @param string $sex + * + * @return string + */ + private function longlifeQuery(string $type, string $sex): string + { + $sex_search = ' 1=1'; + if ($sex === 'F') { + $sex_search = " i_sex='F'"; + } elseif ($sex === 'M') { + $sex_search = " i_sex='M'"; + } + + $rows = $this->runSql( + " SELECT" . + " 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->id()} 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(); + } 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 = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>'; + break; + } + + return $result; + } + + /** + * Find the longest lived individual. + * + * @return string + */ + public function longestLife(): string + { + return $this->longlifeQuery('full', 'BOTH'); + } + + /** + * Find the age of the longest lived individual. + * + * @return string + */ + public function longestLifeAge(): string + { + return $this->longlifeQuery('age', 'BOTH'); + } + + /** + * Find the name of the longest lived individual. + * + * @return string + */ + public function longestLifeName(): string + { + return $this->longlifeQuery('name', 'BOTH'); + } + + /** + * Find the longest lived female. + * + * @return string + */ + public function longestLifeFemale(): string + { + return $this->longlifeQuery('full', 'F'); + } + + /** + * Find the age of the longest lived female. + * + * @return string + */ + public function longestLifeFemaleAge(): string + { + return $this->longlifeQuery('age', 'F'); + } + + /** + * Find the name of the longest lived female. + * + * @return string + */ + public function longestLifeFemaleName(): string + { + return $this->longlifeQuery('name', 'F'); + } + + /** + * Find the longest lived male. + * + * @return string + */ + public function longestLifeMale(): string + { + return $this->longlifeQuery('full', 'M'); + } + + /** + * Find the age of the longest lived male. + * + * @return string + */ + public function longestLifeMaleAge(): string + { + return $this->longlifeQuery('age', 'M'); + } + + /** + * Find the name of the longest lived male. + * + * @return string + */ + public function longestLifeMaleName(): string + { + return $this->longlifeQuery('name', 'M'); + } + + /** + * Returns the calculated age the time of event. + * + * @param int $age The age from the database record + * + * @return string + */ + private function calculateAge(int $age): string + { + if ((int) ($age / 365.25) > 0) { + $result = (int) ($age / 365.25) . 'y'; + } elseif ((int) ($age / 30.4375) > 0) { + $result = (int) ($age / 30.4375) . 'm'; + } else { + $result = $age . 'd'; + } + + return FunctionsDate::getAgeAtEvent($result); + } + + /** + * Find the oldest individuals. + * + * @param string $sex + * @param int $total + * + * @return array + */ + private function topTenOldestQuery(string $sex, int $total): array + { + 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 " . + " 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->id()} 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 = []; + foreach ($rows as $row) { + $person = Individual::getInstance($row->deathdate, $this->tree); + + if ($person->canShow()) { + $top10[] = [ + 'person' => $person, + 'age' => $this->calculateAge((int) $row->age), + ]; + } + } + + // TODO +// if (I18N::direction() === 'rtl') { +// $top10 = str_replace([ +// '[', +// ']', +// '(', +// ')', +// '+', +// ], [ +// '‏[', +// '‏]', +// '‏(', +// '‏)', +// '‏+', +// ], $top10); +// } + + return $top10; + } + + /** + * Find the oldest individuals. + * + * @param int $total + * + * @return string + */ + public function topTenOldest(int $total = 10): string + { + $records = $this->topTenOldestQuery('BOTH', $total); + + return view( + 'statistics/individuals/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest living individuals. + * + * @param int $total + * + * @return string + */ + public function topTenOldestList(int $total = 10): string + { + $records = $this->topTenOldestQuery('BOTH', $total); + + return view( + 'statistics/individuals/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest females. + * + * @param int $total + * + * @return string + */ + public function topTenOldestFemale(int $total = 10): string + { + $records = $this->topTenOldestQuery('F', $total); + + return view( + 'statistics/individuals/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest living females. + * + * @param int $total + * + * @return string + */ + public function topTenOldestFemaleList(int $total = 10): string + { + $records = $this->topTenOldestQuery('F', $total); + + return view( + 'statistics/individuals/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the longest lived males. + * + * @param int $total + * + * @return string + */ + public function topTenOldestMale(int $total = 10): string + { + $records = $this->topTenOldestQuery('M', $total); + + return view( + 'statistics/individuals/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the longest lived males. + * + * @param int $total + * + * @return string + */ + public function topTenOldestMaleList(int $total = 10): string + { + $records = $this->topTenOldestQuery('M', $total); + + return view( + 'statistics/individuals/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest living individuals. + * + * @param string $sex + * @param int $total + * + * @return array + */ + private function topTenOldestAliveQuery(string $sex = 'BOTH', int $total = 10): array + { + 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" . + " 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 (" . implode('|', Gedcom::DEATH_EVENTS) . ")' AND" . + " birth.d_file={$this->tree->id()} 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 = []; + + foreach ($rows as $row) { + $person = Individual::getInstance($row->id, $this->tree); + + $top10[] = [ + 'person' => $person, + 'age' => $this->calculateAge(WT_CLIENT_JD - ((int) $row->age)), + ]; + } + + // TODO +// if (I18N::direction() === 'rtl') { +// $top10 = str_replace([ +// '[', +// ']', +// '(', +// ')', +// '+', +// ], [ +// '‏[', +// '‏]', +// '‏(', +// '‏)', +// '‏+', +// ], $top10); +// } + + return $top10; + } + + /** + * Find the oldest living individuals. + * + * @param int $total + * + * @return string + */ + public function topTenOldestAlive(int $total = 10): string + { + if (!Auth::isMember($this->tree)) { + return I18N::translate('This information is private and cannot be shown.'); + } + + $records = $this->topTenOldestAliveQuery('BOTH', $total); + + return view( + 'statistics/individuals/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest living individuals. + * + * @param int $total + * + * @return string + */ + public function topTenOldestListAlive(int $total = 10): string + { + if (!Auth::isMember($this->tree)) { + return I18N::translate('This information is private and cannot be shown.'); + } + + $records = $this->topTenOldestAliveQuery('BOTH', $total); + + return view( + 'statistics/individuals/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest living females. + * + * @param int $total + * + * @return string + */ + public function topTenOldestFemaleAlive(int $total = 10): string + { + if (!Auth::isMember($this->tree)) { + return I18N::translate('This information is private and cannot be shown.'); + } + + $records = $this->topTenOldestAliveQuery('F', $total); + + return view( + 'statistics/individuals/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the oldest living females. + * + * @param int $total + * + * @return string + */ + public function topTenOldestFemaleListAlive(int $total = 10): string + { + if (!Auth::isMember($this->tree)) { + return I18N::translate('This information is private and cannot be shown.'); + } + + $records = $this->topTenOldestAliveQuery('F', $total); + + return view( + 'statistics/individuals/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the longest lived living males. + * + * @param int $total + * + * @return string + */ + public function topTenOldestMaleAlive(int $total = 10): string + { + if (!Auth::isMember($this->tree)) { + return I18N::translate('This information is private and cannot be shown.'); + } + + $records = $this->topTenOldestAliveQuery('M', $total); + + return view( + 'statistics/individuals/top10-nolist', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the longest lived living males. + * + * @param int $total + * + * @return string + */ + public function topTenOldestMaleListAlive(int $total = 10): string + { + if (!Auth::isMember($this->tree)) { + return I18N::translate('This information is private and cannot be shown.'); + } + + $records = $this->topTenOldestAliveQuery('M', $total); + + return view( + 'statistics/individuals/top10-list', + [ + 'records' => $records, + ] + ); + } + + /** + * Find the average lifespan. + * + * @param string $sex + * @param bool $show_years + * + * @return string + */ + private function averageLifespanQuery(string $sex = 'BOTH', bool $show_years = false): string + { + 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 IFNULL(AVG(death.d_julianday2-birth.d_julianday1), 0) 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->id() . " 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 + ); + + $age = $rows[0]->age; + + if ($show_years) { + return $this->calculateAge((int) $rows[0]->age); + } + + return I18N::number($age / 365.25); + } + + /** + * Find the average lifespan. + * + * @param bool $show_years + * + * @return string + */ + public function averageLifespan($show_years = false): string + { + return $this->averageLifespanQuery('BOTH', $show_years); + } + + /** + * Find the average lifespan of females. + * + * @param bool $show_years + * + * @return string + */ + public function averageLifespanFemale($show_years = false): string + { + return $this->averageLifespanQuery('F', $show_years); + } + + /** + * Find the average male lifespan. + * + * @param bool $show_years + * + * @return string + */ + public function averageLifespanMale($show_years = false): string + { + return $this->averageLifespanQuery('M', $show_years); + } + + /** + * Convert totals into percentages. + * + * @param int $count + * @param int $total + * + * @return string + */ + private function getPercentage(int $count, int $total): string + { + return I18N::percentage($count / $total, 1); + } + + /** + * Returns how many individuals exist in the tree. + * + * @return int + */ + private function totalIndividualsQuery(): int + { + return DB::table('individuals') + ->where('i_file', '=', $this->tree->id()) + ->count(); + } + + /** + * 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(): int + { + return DB::table('individuals') + ->where('i_file', '=', $this->tree->id()) + ->where( + 'i_gedcom', + 'not regexp', + "\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ')' + ) + ->count(); + } + + /** + * Count the number of dead individuals. + * + * @return int + */ + private function totalDeceasedQuery(): int + { + return DB::table('individuals') + ->where('i_file', '=', $this->tree->id()) + ->where( + 'i_gedcom', + 'regexp', + "\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ')' + ) + ->count(); + } + + /** + * Returns the total count of a specific sex. + * + * @param string $sex The sex to query + * + * @return int + */ + private function getTotalSexQuery(string $sex): int + { + return DB::table('individuals') + ->where('i_file', '=', $this->tree->id()) + ->where('i_sex', '=', $sex) + ->count(); + } + + /** + * Returns the total number of males. + * + * @return int + */ + private function totalSexMalesQuery(): int + { + return $this->getTotalSexQuery('M'); + } + + /** + * Returns the total number of females. + * + * @return int + */ + private function totalSexFemalesQuery(): int + { + return $this->getTotalSexQuery('F'); + } + + /** + * Returns the total number of individuals with unknown sex. + * + * @return int + */ + private function totalSexUnknownQuery(): int + { + return $this->getTotalSexQuery('U'); + } + + /** + * Count the total families. + * + * @return int + */ + private function totalFamiliesQuery(): int + { + return DB::table('families') + ->where('f_file', '=', $this->tree->id()) + ->count(); + } + + /** + * How many individuals have one or more sources. + * + * @return int + */ + private function totalIndisWithSourcesQuery(): int + { + return DB::table('individuals') + ->select(['i_id']) + ->distinct() + ->join('link', function (JoinClause $join) { + $join->on('i_id', '=', 'l_from') + ->on('i_file', '=', 'l_file'); + }) + ->where('l_file', '=', $this->tree->id()) + ->where('l_type', '=', 'SOUR') + ->count('i_id'); + } + + /** + * Count the families with source records. + * + * @return int + */ + private function totalFamsWithSourcesQuery(): int + { + return DB::table('families') + ->select(['f_id']) + ->distinct() + ->join('link', function (JoinClause $join) { + $join->on('f_id', '=', 'l_from') + ->on('f_file', '=', 'l_file'); + }) + ->where('l_file', '=', $this->tree->id()) + ->where('l_type', '=', 'SOUR') + ->count('f_id'); + } + + /** + * Count the number of repositories. + * + * @return int + */ + private function totalRepositoriesQuery(): int + { + return DB::table('other') + ->where('o_file', '=', $this->tree->id()) + ->where('o_type', '=', 'REPO') + ->count(); + } + + /** + * Count the total number of sources. + * + * @return int + */ + private function totalSourcesQuery(): int + { + return DB::table('sources') + ->where('s_file', '=', $this->tree->id()) + ->count(); + } + + /** + * Count the number of notes. + * + * @return int + */ + private function totalNotesQuery(): int + { + return DB::table('other') + ->where('o_file', '=', $this->tree->id()) + ->where('o_type', '=', 'NOTE') + ->count(); + } + + /** + * Returns the total number of records. + * + * @return int + */ + private function totalRecordsQuery(): int + { + return $this->totalIndividualsQuery() + + $this->totalFamiliesQuery() + + $this->totalNotesQuery() + + $this->totalRepositoriesQuery() + + $this->totalSourcesQuery(); + } + + /** + * @inheritDoc + */ + public function totalRecords(): string + { + return I18N::number($this->totalRecordsQuery()); + } + + /** + * @inheritDoc + */ + public function totalIndividuals(): string + { + return I18N::number($this->totalIndividualsQuery()); + } + + /** + * Count the number of living individuals. + * + * @return string + */ + public function totalLiving(): string + { + return I18N::number($this->totalLivingQuery()); + } + + /** + * Count the number of dead individuals. + * + * @return string + */ + public function totalDeceased(): string + { + return I18N::number($this->totalDeceasedQuery()); + } + + /** + * @inheritDoc + */ + public function totalSexMales(): string + { + return I18N::number($this->totalSexMalesQuery()); + } + + /** + * @inheritDoc + */ + public function totalSexFemales(): string + { + return I18N::number($this->totalSexFemalesQuery()); + } + + /** + * @inheritDoc + */ + public function totalSexUnknown(): string + { + return I18N::number($this->totalSexUnknownQuery()); + } + + /** + * @inheritDoc + */ + public function totalFamilies(): string + { + return I18N::number($this->totalFamiliesQuery()); + } + + /** + * How many individuals have one or more sources. + * + * @return string + */ + public function totalIndisWithSources(): string + { + return I18N::number($this->totalIndisWithSourcesQuery()); + } + + /** + * Count the families with with source records. + * + * @return string + */ + public function totalFamsWithSources(): string + { + return I18N::number($this->totalFamsWithSourcesQuery()); + } + + /** + * @inheritDoc + */ + public function totalRepositories(): string + { + return I18N::number($this->totalRepositoriesQuery()); + } + + /** + * @inheritDoc + */ + public function totalSources(): string + { + return I18N::number($this->totalSourcesQuery()); + } + + /** + * @inheritDoc + */ + public function totalNotes(): string + { + return I18N::number($this->totalNotesQuery()); + } + + /** + * @inheritDoc + */ + public function totalIndividualsPercentage(): string + { + return $this->getPercentage( + $this->totalIndividualsQuery(), + $this->totalRecordsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalFamiliesPercentage(): string + { + return $this->getPercentage( + $this->totalFamiliesQuery(), + $this->totalRecordsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalRepositoriesPercentage(): string + { + return $this->getPercentage( + $this->totalRepositoriesQuery(), + $this->totalRecordsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalSourcesPercentage(): string + { + return $this->getPercentage( + $this->totalSourcesQuery(), + $this->totalRecordsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalNotesPercentage(): string + { + return $this->getPercentage( + $this->totalNotesQuery(), + $this->totalRecordsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalLivingPercentage(): string + { + return $this->getPercentage( + $this->totalLivingQuery(), + $this->totalIndividualsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalDeceasedPercentage(): string + { + return $this->getPercentage( + $this->totalDeceasedQuery(), + $this->totalIndividualsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalSexMalesPercentage(): string + { + return $this->getPercentage( + $this->totalSexMalesQuery(), + $this->totalIndividualsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalSexFemalesPercentage(): string + { + return $this->getPercentage( + $this->totalSexFemalesQuery(), + $this->totalIndividualsQuery() + ); + } + + /** + * @inheritDoc + */ + public function totalSexUnknownPercentage(): string + { + return $this->getPercentage( + $this->totalSexUnknownQuery(), + $this->totalIndividualsQuery() + ); + } + + /** + * Create a chart of common given names. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * @param int $maxtoshow + * + * @return string + */ + public function chartCommonGiven( + string $size = null, + string $color_from = null, + string $color_to = null, + int $maxtoshow = 7 + ): string { + $tot_indi = $this->totalIndividualsQuery(); + $given = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow); + + return (new ChartCommonGiven()) + ->chartCommonGiven($tot_indi, $given, $size, $color_from, $color_to); + } + + /** + * Create a chart of common surnames. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * @param int $number_of_surnames + * + * @return string + */ + public function chartCommonSurnames( + string $size = null, + string $color_from = null, + string $color_to = null, + int $number_of_surnames = 10 + ): string { + $tot_indi = $this->totalIndividualsQuery(); + $all_surnames = $this->topSurnames($number_of_surnames, 0); + + return (new ChartCommonSurname($this->tree)) + ->chartCommonSurnames($tot_indi, $all_surnames, $size, $color_from, $color_to); + } + + /** + * Create a chart showing mortality. + * + * @param string|null $size + * @param string|null $color_living + * @param string|null $color_dead + * + * @return string + */ + public function chartMortality(string $size = null, string $color_living = null, string $color_dead = null): string + { + $tot_l = $this->totalLivingQuery(); + $tot_d = $this->totalDeceasedQuery(); + + return (new ChartMortality($this->tree)) + ->chartMortality($tot_l, $tot_d, $size, $color_living, $color_dead); + } + + /** + * Create a chart showing individuals with/without sources. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartIndisWithSources( + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + $tot_indi = $this->totalIndividualsQuery(); + $tot_indi_source = $this->totalIndisWithSourcesQuery(); + + return (new ChartIndividual()) + ->chartIndisWithSources($tot_indi, $tot_indi_source, $size, $color_from, $color_to); + } + + /** + * Create a chart of individuals with/without sources. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartFamsWithSources( + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + $tot_fam = $this->totalFamiliesQuery(); + $tot_fam_source = $this->totalFamsWithSourcesQuery(); + + return (new ChartFamilyWithSources()) + ->chartFamsWithSources($tot_fam, $tot_fam_source, $size, $color_from, $color_to); + } + + /** + * @inheritDoc + */ + public function chartSex( + string $size = null, + string $color_female = null, + string $color_male = null, + string $color_unknown = null + ): string { + $tot_m = $this->totalSexMalesQuery(); + $tot_f = $this->totalSexFemalesQuery(); + $tot_u = $this->totalSexUnknownQuery(); + + return (new ChartSex($this->tree)) + ->chartSex($tot_m, $tot_f, $tot_u, $size, $color_female, $color_male, $color_unknown); + } +} diff --git a/app/Statistics/Repository/Interfaces/BrowserRepositoryInterface.php b/app/Statistics/Repository/Interfaces/BrowserRepositoryInterface.php new file mode 100644 index 0000000000..ba22b7e1c9 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/BrowserRepositoryInterface.php @@ -0,0 +1,45 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for browser related statistics. + */ +interface BrowserRepositoryInterface +{ + /** + * What is the client's date. + * + * @return string + */ + public function browserDate(): string; + + /** + * What is the client's timestamp. + * + * @return string + */ + public function browserTime(): string; + + /** + * What is the browser's tiemzone. + * + * @return string + */ + public function browserTimezone(): string; +} diff --git a/app/Statistics/Repository/Interfaces/ContactRepositoryInterface.php b/app/Statistics/Repository/Interfaces/ContactRepositoryInterface.php new file mode 100644 index 0000000000..d85ddb79c0 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/ContactRepositoryInterface.php @@ -0,0 +1,38 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for contact related statistics. + */ +interface ContactRepositoryInterface +{ + /** + * Returns a link to contact the webmaster. + * + * @return string + */ + public function contactWebmaster(): string; + + /** + * Returns a link to contact the genealogy contact. + * + * @return string + */ + public function contactGedcom(): string; +} diff --git a/app/Statistics/Repository/Interfaces/EventRepositoryInterface.php b/app/Statistics/Repository/Interfaces/EventRepositoryInterface.php new file mode 100644 index 0000000000..4c79f9dee1 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/EventRepositoryInterface.php @@ -0,0 +1,166 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for event related statistics. + */ +interface EventRepositoryInterface +{ + /** + * Count the number of events (with dates). + * + * @param string[] $events + * + * @return string + */ + public function totalEvents(array $events = []): string; + + /** + * Count the number of births events (BIRT, CHR, BAPM, ADOP). + * + * @return string + */ + public function totalEventsBirth(): string; + + /** + * Count the number of births (BIRT). + * + * @return string + */ + public function totalBirths(): string; + + /** + * Count the number of death events (DEAT, BURI, CREM). + * + * @return string + */ + public function totalEventsDeath(): string; + + /** + * Count the number of deaths (DEAT). + * + * @return string + */ + public function totalDeaths(): string; + + /** + * Count the number of marriage events (MARR, _NMR). + * + * @return string + */ + public function totalEventsMarriage(): string; + + /** + * Count the number of marriages (MARR). + * + * @return string + */ + public function totalMarriages(): string; + + /** + * Count the number of divorce events (DIV, ANUL, _SEPR). + * + * @return string + */ + public function totalEventsDivorce(): string; + + /** + * Count the number of divorces (DIV). + * + * @return string + */ + public function totalDivorces(): string; + + /** + * Count the number of other events (not birth, death, marriage or divorce related). + * + * @return string + */ + public function totalEventsOther(): string; + + /** + * Find the earliest event. + * + * @return string + */ + public function firstEvent(): string; + + /** + * Find the latest event. + * + * @return string + */ + public function lastEvent(): string; + + /** + * Find the year of the earliest event. + * + * @return string + */ + public function firstEventYear(): string; + + /** + * Find the year of the latest event. + * + * @return string + */ + public function lastEventYear(): string; + + /** + * Find the type of the earliest event. + * + * @return string + */ + public function firstEventType(): string; + + /** + * Find the type of the latest event. + * + * @return string + */ + public function lastEventType(): string; + + /** + * Find the name of the individual with the earliest event. + * + * @return string + */ + public function firstEventName(): string; + + /** + * Find the name of the individual with the latest event. + * + * @return string + */ + public function lastEventName(): string; + + /** + * Find the location of the earliest event. + * + * @return string + */ + public function firstEventPlace(): string; + + /** + * Find the location of the latest event. + * + * @return string + */ + public function lastEventPlace(): string; +} diff --git a/app/Statistics/Repository/Interfaces/FamilyDatesRepositoryInterface.php b/app/Statistics/Repository/Interfaces/FamilyDatesRepositoryInterface.php new file mode 100644 index 0000000000..46cf17af97 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/FamilyDatesRepositoryInterface.php @@ -0,0 +1,248 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for family dates related statistics (birth, death, marriage, divorce). + */ +interface FamilyDatesRepositoryInterface +{ + /** + * Find the earliest birth. + * + * @return string + */ + public function firstBirth(): string; + + /** + * Find the earliest birth year. + * + * @return string + */ + public function firstBirthYear(): string; + + /** + * Find the name of the earliest birth. + * + * @return string + */ + public function firstBirthName(): string; + + /** + * Find the earliest birth place. + * + * @return string + */ + public function firstBirthPlace(): string; + + /** + * Find the latest birth. + * + * @return string + */ + public function lastBirth(): string; + + /** + * Find the latest birth year. + * + * @return string + */ + public function lastBirthYear(): string; + + /** + * Find the latest birth name. + * + * @return string + */ + public function lastBirthName(): string; + + /** + * Find the latest birth place. + * + * @return string + */ + public function lastBirthPlace(): string; + + /** + * Find the earliest death. + * + * @return string + */ + public function firstDeath(): string; + + /** + * Find the earliest death year. + * + * @return string + */ + public function firstDeathYear(): string; + + /** + * Find the earliest death name. + * + * @return string + */ + public function firstDeathName(): string; + + /** + * Find the earliest death place. + * + * @return string + */ + public function firstDeathPlace(): string; + + /** + * Find the latest death. + * + * @return string + */ + public function lastDeath(): string; + + /** + * Find the latest death year. + * + * @return string + */ + public function lastDeathYear(): string; + + /** + * Find the latest death name. + * + * @return string + */ + public function lastDeathName(): string; + + /** + * Find the place of the latest death. + * + * @return string + */ + public function lastDeathPlace(): string; + + /** + * Find the earliest marriage. + * + * @return string + */ + public function firstMarriage(): string; + + /** + * Find the year of the earliest marriage. + * + * @return string + */ + public function firstMarriageYear(): string; + + /** + * Find the names of spouses of the earliest marriage. + * + * @return string + */ + public function firstMarriageName(): string; + + /** + * Find the place of the earliest marriage. + * + * @return string + */ + public function firstMarriagePlace(): string; + + /** + * Find the latest marriage. + * + * @return string + */ + public function lastMarriage(): string; + + /** + * Find the year of the latest marriage. + * + * @return string + */ + public function lastMarriageYear(): string; + + /** + * Find the names of spouses of the latest marriage. + * + * @return string + */ + public function lastMarriageName(): string; + + /** + * Find the location of the latest marriage. + * + * @return string + */ + public function lastMarriagePlace(): string; + + /** + * Find the earliest divorce. + * + * @return string + */ + public function firstDivorce(): string; + + /** + * Find the year of the earliest divorce. + * + * @return string + */ + public function firstDivorceYear(): string; + + /** + * Find the names of individuals in the earliest divorce. + * + * @return string + */ + public function firstDivorceName(): string; + + /** + * Find the location of the earliest divorce. + * + * @return string + */ + public function firstDivorcePlace(): string; + + /** + * Find the latest divorce. + * + * @return string + */ + public function lastDivorce(): string; + + /** + * Find the year of the latest divorce. + * + * @return string + */ + public function lastDivorceYear(): string; + + /** + * Find the names of the individuals in the latest divorce. + * + * @return string + */ + public function lastDivorceName(): string; + + /** + * Find the location of the latest divorce. + * + * @return string + */ + public function lastDivorcePlace(): string; +} diff --git a/app/Statistics/Repository/Interfaces/FavoritesRepositoryInterface.php b/app/Statistics/Repository/Interfaces/FavoritesRepositoryInterface.php new file mode 100644 index 0000000000..1d85ee2ea0 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/FavoritesRepositoryInterface.php @@ -0,0 +1,52 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for favorites related statistics. + */ +interface FavoritesRepositoryInterface +{ + /** + * Find the favorites for the tree. + * + * @return string + */ + public function gedcomFavorites(): string; + + /** + * Find the favorites for the user. + * + * @return string + */ + public function userFavorites(): string; + + /** + * Find the number of favorites for the tree. + * + * @return string + */ + public function totalGedcomFavorites(): string; + + /** + * Find the number of favorites for the user. + * + * @return string + */ + public function totalUserFavorites(): string; +} diff --git a/app/Statistics/Repository/Interfaces/GedcomRepositoryInterface.php b/app/Statistics/Repository/Interfaces/GedcomRepositoryInterface.php new file mode 100644 index 0000000000..f949a40f2c --- /dev/null +++ b/app/Statistics/Repository/Interfaces/GedcomRepositoryInterface.php @@ -0,0 +1,80 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for GEDCOM related statistics. + */ +interface GedcomRepositoryInterface +{ + /** + * Get the name used for GEDCOM files and URLs. + * + * @return string + */ + public function gedcomFilename(): string; + + /** + * Get the internal ID number of the tree. + * + * @return int + */ + public function gedcomId(): int; + + /** + * Get the descriptive title of the tree. + * + * @return string + */ + public function gedcomTitle(): string; + + /** + * Get the software originally used to create the GEDCOM file. + * + * @return string + */ + public function gedcomCreatedSoftware(): string; + + /** + * Get the version of software which created the GEDCOM file. + * + * @return string + */ + public function gedcomCreatedVersion(): string; + + /** + * Get the date the GEDCOM file was created. + * + * @return string + */ + public function gedcomDate(): string; + + /** + * When was this tree last updated? + * + * @return string + */ + public function gedcomUpdated(): string; + + /** + * What is the significant individual from this tree? + * + * @return string + */ + public function gedcomRootId(): string; +} diff --git a/app/Statistics/Repository/Interfaces/HitCountRepositoryInterface.php b/app/Statistics/Repository/Interfaces/HitCountRepositoryInterface.php new file mode 100644 index 0000000000..c22a199d9e --- /dev/null +++ b/app/Statistics/Repository/Interfaces/HitCountRepositoryInterface.php @@ -0,0 +1,96 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for hit count related statistics. + */ +interface HitCountRepositoryInterface +{ + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCount(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountUser(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountIndi(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountFam(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountSour(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountRepo(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountNote(string $page_parameter = ''): string; + + /** + * How many times has a page been viewed. + * + * @param string $page_parameter + * + * @return string + */ + public function hitCountObje(string $page_parameter = ''): string; +} diff --git a/app/Statistics/Repository/Interfaces/IndividualRepositoryInterface.php b/app/Statistics/Repository/Interfaces/IndividualRepositoryInterface.php new file mode 100644 index 0000000000..1bad0df3e1 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/IndividualRepositoryInterface.php @@ -0,0 +1,174 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for individual related statistics. + */ +interface IndividualRepositoryInterface +{ + /** + * How many GEDCOM records exist in the tree. + * + * @return string + */ + public function totalRecords(): string; + + /** + * How many individuals exist in the tree. + * + * @return string + */ + public function totalIndividuals(): string; + + /** + * Count the number of males. + * + * @return string + */ + public function totalSexMales(): string; + + /** + * Count the number of females. + * + * @return string + */ + public function totalSexFemales(): string; + + /** + * Count the number of individuals with unknown sex. + * + * @return string + */ + public function totalSexUnknown(): string; + + /** + * Count the total families. + * + * @return string + */ + public function totalFamilies(): string; + + /** + * Count the number of repositories + * + * @return string + */ + public function totalRepositories(): string; + + /** + * Count the total number of sources. + * + * @return string + */ + public function totalSources(): string; + + /** + * Count the number of notes. + * + * @return string + */ + public function totalNotes(): string; + + /** + * Show the total individuals as a percentage. + * + * @return string + */ + public function totalIndividualsPercentage(): string; + + /** + * Show the total families as a percentage. + * + * @return string + */ + public function totalFamiliesPercentage(): string; + + /** + * Show the total number of repositories as a percentage. + * + * @return string + */ + public function totalRepositoriesPercentage(): string; + + /** + * Show the number of sources as a percentage. + * + * @return string + */ + public function totalSourcesPercentage(): string; + + /** + * Show the number of notes as a percentage. + * + * @return string + */ + public function totalNotesPercentage(): string; + + /** + * Count the number of living individuals. + * + * @return string + */ + public function totalLivingPercentage(): string; + + /** + * Count the number of dead individuals. + * + * @return string + */ + public function totalDeceasedPercentage(): string; + + /** + * Count the number of males + * + * @return string + */ + public function totalSexMalesPercentage(): string; + + /** + * Count the number of females. + * + * @return string + */ + public function totalSexFemalesPercentage(): string; + + /** + * Count the number of individuals with unknown sex. + * + * @return string + */ + public function totalSexUnknownPercentage(): string; + + /** + * Generate a chart showing sex distribution. + * + * @param string|null $size + * @param string|null $color_female + * @param string|null $color_male + * @param string|null $color_unknown + * + * @return string + */ + public function chartSex( + string $size = null, + string $color_female = null, + string $color_male = null, + string $color_unknown = null + ): string; +} diff --git a/app/Statistics/Repository/Interfaces/LatestUserRepositoryInterface.php b/app/Statistics/Repository/Interfaces/LatestUserRepositoryInterface.php new file mode 100644 index 0000000000..48fe34e392 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/LatestUserRepositoryInterface.php @@ -0,0 +1,73 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for latest user related statistics. + */ +interface LatestUserRepositoryInterface +{ + /** + * Get the newest registered user's ID. + * + * @return string + */ + public function latestUserId(): string; + + /** + * Get the newest registered user's username. + * + * @return string + */ + public function latestUserName(): string; + + /** + * Get the newest registered user's real name. + * + * @return string + */ + public function latestUserFullName(): string; + + /** + * Get the date of the newest user registration. + * + * @param string|null $format + * + * @return string + */ + public function latestUserRegDate(string $format = null): string; + + /** + * Find the timestamp of the latest user to register. + * + * @param string|null $format + * + * @return string + */ + public function latestUserRegTime(string $format = null): string; + + /** + * Is the most recently registered user logged in right now? + * + * @param string|null $yes + * @param string|null $no + * + * @return string + */ + public function latestUserLoggedin(string $yes = null, string $no = null): string; +} diff --git a/app/Statistics/Repository/Interfaces/MediaRepositoryInterface.php b/app/Statistics/Repository/Interfaces/MediaRepositoryInterface.php new file mode 100644 index 0000000000..ac7cd69f52 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/MediaRepositoryInterface.php @@ -0,0 +1,175 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for media type related statistics. + */ +interface MediaRepositoryInterface +{ + /** + * Count the number of media records. + * + * @return string + */ + public function totalMedia(): string; + + /** + * Count the number of media records with type "audio". + * + * @return string + */ + public function totalMediaAudio(): string; + + /** + * Count the number of media records with type "book". + * + * @return string + */ + public function totalMediaBook(): string; + + /** + * Count the number of media records with type "card". + * + * @return string + */ + public function totalMediaCard(): string; + + /** + * Count the number of media records with type "certificate". + * + * @return string + */ + public function totalMediaCertificate(): string; + + /** + * Count the number of media records with type "coat of arms". + * + * @return string + */ + public function totalMediaCoatOfArms(): string; + + /** + * Count the number of media records with type "document". + * + * @return string + */ + public function totalMediaDocument(): string; + + /** + * Count the number of media records with type "electronic". + * + * @return string + */ + public function totalMediaElectronic(): string; + + /** + * Count the number of media records with type "magazine". + * + * @return string + */ + public function totalMediaMagazine(): string; + + /** + * Count the number of media records with type "manuscript". + * + * @return string + */ + public function totalMediaManuscript(): string; + + /** + * Count the number of media records with type "map". + * + * @return string + */ + public function totalMediaMap(): string; + + /** + * Count the number of media records with type "microfiche". + * + * @return string + */ + public function totalMediaFiche(): string; + + /** + * Count the number of media records with type "microfilm". + * + * @return string + */ + public function totalMediaFilm(): string; + + /** + * Count the number of media records with type "newspaper". + * + * @return string + */ + public function totalMediaNewspaper(): string; + + /** + * Count the number of media records with type "painting". + * + * @return string + */ + public function totalMediaPainting(): string; + + /** + * Count the number of media records with type "photograph". + * + * @return string + */ + public function totalMediaPhoto(): string; + + /** + * Count the number of media records with type "tombstone". + * + * @return string + */ + public function totalMediaTombstone(): string; + + /** + * Count the number of media records with type "video". + * + * @return string + */ + public function totalMediaVideo(): string; + + /** + * Count the number of media records with type "other". + * + * @return string + */ + public function totalMediaOther(): string; + + /** + * Count the number of media records with type "unknown". + * + * @return string + */ + public function totalMediaUnknown(): string; + + /** + * Create a chart of media types. + * + * @param string|null $size + * @param string|null $color_from + * @param string|null $color_to + * + * @return string + */ + public function chartMedia(string $size = null, string $color_from = null, string $color_to = null): string; +} diff --git a/app/Statistics/Repository/Interfaces/MessageRepositoryInterface.php b/app/Statistics/Repository/Interfaces/MessageRepositoryInterface.php new file mode 100644 index 0000000000..dcb0c30e84 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/MessageRepositoryInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for user message related statistics. + */ +interface MessageRepositoryInterface +{ + /** + * How many messages in the user's inbox. + * + * @return string + */ + public function totalUserMessages(): string; +} diff --git a/app/Statistics/Repository/Interfaces/NewsRepositoryInterface.php b/app/Statistics/Repository/Interfaces/NewsRepositoryInterface.php new file mode 100644 index 0000000000..227c811ba6 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/NewsRepositoryInterface.php @@ -0,0 +1,38 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for news related statistics. + */ +interface NewsRepositoryInterface +{ + /** + * How many blog entries exist for this user. + * + * @return string + */ + public function totalUserJournal(): string; + + /** + * How many news items exist for this tree. + * + * @return string + */ + public function totalGedcomNews(): string; +} diff --git a/app/Statistics/Repository/Interfaces/PlaceRepositoryInterface.php b/app/Statistics/Repository/Interfaces/PlaceRepositoryInterface.php new file mode 100644 index 0000000000..804cfe2c05 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/PlaceRepositoryInterface.php @@ -0,0 +1,86 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for place related statistics. + */ +interface PlaceRepositoryInterface +{ + /** + * Places + * + * @param string $what + * @param string $fact + * @param int $parent + * @param bool $country + * + * @return int[]|\stdClass[] + */ + public function statsPlaces(string $what = 'ALL', string $fact = '', int $parent = 0, bool $country = false): array; + + /** + * A list of common birth places. + * + * @return string + */ + public function commonBirthPlacesList(): string; + + /** + * A list of common death places. + * + * @return string + */ + public function commonDeathPlacesList(): string; + + /** + * A list of common marriage places. + * + * @return string + */ + public function commonMarriagePlacesList(): string; + + /** + * A list of common countries. + * + * @return string + */ + public function commonCountriesList(): string; + + /** + * Count total places. + * + * @return string + */ + public function totalPlaces(): string; + + /** + * Create a chart showing where events occurred. + * + * @param string $chart_shows + * @param string $chart_type + * @param string $surname + * + * @return string + */ + public function chartDistribution( + string $chart_shows = 'world', + string $chart_type = '', + string $surname = '' + ) : string; +} diff --git a/app/Statistics/Repository/Interfaces/ServerRepositoryInterface.php b/app/Statistics/Repository/Interfaces/ServerRepositoryInterface.php new file mode 100644 index 0000000000..4d0400f3d9 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/ServerRepositoryInterface.php @@ -0,0 +1,52 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for server related statistics. + */ +interface ServerRepositoryInterface +{ + /** + * What is the current date on the server? + * + * @return string + */ + public function serverDate(): string; + + /** + * What is the current time on the server (in 12 hour clock)? + * + * @return string + */ + public function serverTime(): string; + + /** + * What is the current time on the server (in 24 hour clock)? + * + * @return string + */ + public function serverTime24(): string; + + /** + * What is the timezone of the server. + * + * @return string + */ + public function serverTimezone(): string; +} diff --git a/app/Statistics/Repository/Interfaces/UserRepositoryInterface.php b/app/Statistics/Repository/Interfaces/UserRepositoryInterface.php new file mode 100644 index 0000000000..66946bdff4 --- /dev/null +++ b/app/Statistics/Repository/Interfaces/UserRepositoryInterface.php @@ -0,0 +1,103 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository\Interfaces; + +/** + * A repository providing methods for user related statistics. + */ +interface UserRepositoryInterface +{ + /** + * Who is currently logged in? + * + * @return string + */ + public function usersLoggedIn(): string; + + /** + * Who is currently logged in? + * + * @return string + */ + public function usersLoggedInList(): string; + + /** + * Returns the total number of logged in users (visible or anonymous). + * + * @return int + */ + public function usersLoggedInTotal(): int; + + /** + * Returns the total number of anonymous logged in users. + * + * @return int + */ + public function usersLoggedInTotalAnon(): int; + + /** + * Returns the total number of visible logged in users. + * + * @return int + */ + public function usersLoggedInTotalVisible(): int; + + /** + * Get the current user's ID. + * + * @return string + */ + public function userId(): string; + + /** + * Get the current user's username. + * + * @param string $visitor_text + * + * @return string + */ + public function userName(string $visitor_text = ''): string; + + /** + * Get the current user's full name. + * + * @return string + */ + public function userFullName(): string; + + /** + * Count the number of users. + * + * @return string + */ + public function totalUsers(): string; + + /** + * Count the number of administrators. + * + * @return string + */ + public function totalAdmins(): string; + + /** + * Count the number of administrators. + * + * @return string + */ + public function totalNonAdmins(): string; +} diff --git a/app/Statistics/Repository/LatestUserRepository.php b/app/Statistics/Repository/LatestUserRepository.php new file mode 100644 index 0000000000..0a3296a4bf --- /dev/null +++ b/app/Statistics/Repository/LatestUserRepository.php @@ -0,0 +1,130 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\Functions\FunctionsDate; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\LatestUserRepositoryInterface; +use Fisharebest\Webtrees\User; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinClause; + +/** + * A repository providing methods for latest user related statistics. + */ +class LatestUserRepository implements LatestUserRepositoryInterface +{ + /** + * Find the newest user on the site. + * + * If no user has registered (i.e. all created by the admin), then + * return the current user. + * + * @return User + */ + private function latestUserQuery(): User + { + static $user; + + if ($user instanceof User) { + return $user; + } + + $user_id = DB::table('user as u') + ->select(['u.user_id']) + ->leftJoin('user_setting as us', function (JoinClause $join) { + $join->on(function (Builder $query) { + $query->whereColumn('u.user_id', '=', 'us.user_id') + ->where('us.setting_name', '=', 'reg_timestamp'); + }); + }) + ->orderByDesc('us.setting_value') + ->value('user_id'); + + $user = User::find($user_id) ?? Auth::user(); + + return $user; + } + + /** + * @inheritDoc + */ + public function latestUserId(): string + { + return (string) $this->latestUserQuery()->id(); + } + + /** + * @inheritDoc + */ + public function latestUserName(): string + { + return e($this->latestUserQuery()->getUserName()); + } + + /** + * @inheritDoc + */ + public function latestUserFullName(): string + { + return e($this->latestUserQuery()->getRealName()); + } + + /** + * @inheritDoc + */ + public function latestUserRegDate(string $format = null): string + { + $format = $format ?? I18N::dateFormat(); + $user = $this->latestUserQuery(); + + return FunctionsDate::timestampToGedcomDate( + (int) $user->getPreference('reg_timestamp') + )->display(false, $format); + } + + /** + * @inheritDoc + */ + public function latestUserRegTime(string $format = null): string + { + $format = $format ?? str_replace('%', '', I18N::timeFormat()); + $user = $this->latestUserQuery(); + + return date($format, (int) $user->getPreference('reg_timestamp')); + } + + /** + * @inheritDoc + */ + public function latestUserLoggedin(string $yes = null, string $no = null): string + { + $yes = $yes ?? I18N::translate('yes'); + $no = $no ?? I18N::translate('no'); + $user = $this->latestUserQuery(); + + $is_logged_in = DB::table('session') + ->selectRaw('1') + ->where('user_id', '=', $user->id()) + ->first(); + + return $is_logged_in ? $yes : $no; + } +} diff --git a/app/Statistics/Repository/MediaRepository.php b/app/Statistics/Repository/MediaRepository.php new file mode 100644 index 0000000000..7ff386e06e --- /dev/null +++ b/app/Statistics/Repository/MediaRepository.php @@ -0,0 +1,365 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Google\ChartMedia; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\MediaRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\Builder; + +/** + * A repository providing methods for media type related statistics. + */ +class MediaRepository implements MediaRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Available media types. + */ + private const MEDIA_TYPE_ALL = 'all'; + private const MEDIA_TYPE_AUDIO = 'audio'; + private const MEDIA_TYPE_BOOK = 'book'; + private const MEDIA_TYPE_CARD = 'card'; + private const MEDIA_TYPE_CERTIFICATE = 'certificate'; + private const MEDIA_TYPE_COAT = 'coat'; + private const MEDIA_TYPE_DOCUMENT = 'document'; + private const MEDIA_TYPE_ELECTRONIC = 'electronic'; + private const MEDIA_TYPE_FICHE = 'fiche'; + private const MEDIA_TYPE_FILM = 'film'; + private const MEDIA_TYPE_MAGAZINE = 'magazine'; + private const MEDIA_TYPE_MANUSCRIPT = 'manuscript'; + private const MEDIA_TYPE_MAP = 'map'; + private const MEDIA_TYPE_NEWSPAPER = 'newspaper'; + private const MEDIA_TYPE_PAINTING = 'painting'; + private const MEDIA_TYPE_PHOTO = 'photo'; + private const MEDIA_TYPE_TOMBSTONE = 'tombstone'; + private const MEDIA_TYPE_VIDEO = 'video'; + private const MEDIA_TYPE_OTHER = 'other'; + private const MEDIA_TYPE_UNKNOWN = 'unknown'; + + /** + * List of GEDCOM media types. + * + * @var string[] + */ + private const MEDIA_TYPES = [ + self::MEDIA_TYPE_AUDIO, + self::MEDIA_TYPE_BOOK, + self::MEDIA_TYPE_CARD, + self::MEDIA_TYPE_CERTIFICATE, + self::MEDIA_TYPE_COAT, + self::MEDIA_TYPE_DOCUMENT, + self::MEDIA_TYPE_ELECTRONIC, + self::MEDIA_TYPE_FICHE, + self::MEDIA_TYPE_FILM, + self::MEDIA_TYPE_MAGAZINE, + self::MEDIA_TYPE_MANUSCRIPT, + self::MEDIA_TYPE_MAP, + self::MEDIA_TYPE_NEWSPAPER, + self::MEDIA_TYPE_PAINTING, + self::MEDIA_TYPE_PHOTO, + self::MEDIA_TYPE_TOMBSTONE, + self::MEDIA_TYPE_VIDEO, + self::MEDIA_TYPE_OTHER, + ]; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Returns the number of media records of the given type. + * + * @param string $type The media type to query + * + * @return int + */ + private function totalMediaTypeQuery(string $type): int + { + if (($type !== self::MEDIA_TYPE_ALL) + && ($type !== self::MEDIA_TYPE_UNKNOWN) + && !\in_array($type, self::MEDIA_TYPES, true) + ) { + return 0; + } + + $query = DB::table('media') + ->where('m_file', '=', $this->tree->id()); + + if ($type !== self::MEDIA_TYPE_ALL) { + if ($type === self::MEDIA_TYPE_UNKNOWN) { + // There has to be a better way then this :( + foreach (self::MEDIA_TYPES as $t) { + // Use function to add brackets + $query->where(function (Builder $query) use ($t) { + $query->where('m_gedcom', 'not like', '%3 TYPE ' . $t . '%') + ->where('m_gedcom', 'not like', '%1 _TYPE ' . $t . '%'); + }); + } + } else { + // Use function to add brackets + $query->where(function (Builder $query) use ($type) { + $query->where('m_gedcom', 'like', '%3 TYPE ' . $type . '%') + ->orWhere('m_gedcom', 'like', '%1 _TYPE ' . $type . '%'); + }); + } + } + + return $query->count(); + } + + /** + * @inheritDoc + */ + public function totalMedia(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_ALL)); + } + + /** + * @inheritDoc + */ + public function totalMediaAudio(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_AUDIO)); + } + + /** + * @inheritDoc + */ + public function totalMediaBook(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_BOOK)); + } + + /** + * @inheritDoc + */ + public function totalMediaCard(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_CARD)); + } + + /** + * @inheritDoc + */ + public function totalMediaCertificate(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_CERTIFICATE)); + } + + /** + * @inheritDoc + */ + public function totalMediaCoatOfArms(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_COAT)); + } + + /** + * @inheritDoc + */ + public function totalMediaDocument(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_DOCUMENT)); + } + + /** + * @inheritDoc + */ + public function totalMediaElectronic(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_ELECTRONIC)); + } + + /** + * @inheritDoc + */ + public function totalMediaFiche(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_FICHE)); + } + + /** + * @inheritDoc + */ + public function totalMediaFilm(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_FILM)); + } + + /** + * @inheritDoc + */ + public function totalMediaMagazine(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_MAGAZINE)); + } + + /** + * @inheritDoc + */ + public function totalMediaManuscript(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_MANUSCRIPT)); + } + + /** + * @inheritDoc + */ + public function totalMediaMap(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_MAP)); + } + + /** + * @inheritDoc + */ + public function totalMediaNewspaper(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_NEWSPAPER)); + } + + /** + * @inheritDoc + */ + public function totalMediaPainting(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_PAINTING)); + } + + /** + * @inheritDoc + */ + public function totalMediaPhoto(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_PHOTO)); + } + + /** + * @inheritDoc + */ + public function totalMediaTombstone(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_TOMBSTONE)); + } + + /** + * @inheritDoc + */ + public function totalMediaVideo(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_VIDEO)); + } + + /** + * @inheritDoc + */ + public function totalMediaOther(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_OTHER)); + } + + /** + * @inheritDoc + */ + public function totalMediaUnknown(): string + { + return I18N::number($this->totalMediaTypeQuery(self::MEDIA_TYPE_UNKNOWN)); + } + + /** + * Returns a sorted list of media types and their total counts. + * + * @param int $tot The total number of media files + * + * @return array + */ + private function getSortedMediaTypeList(int $tot): array + { + $media = []; + $c = 0; + $max = 0; + + foreach (self::MEDIA_TYPES as $type) { + $count = $this->totalMediaTypeQuery($type); + + if ($count > 0) { + $media[$type] = $count; + + if ($count > $max) { + $max = $count; + } + + $c += $count; + } + } + + $count = $this->totalMediaTypeQuery(self::MEDIA_TYPE_UNKNOWN); + if ($count > 0) { + $media[self::MEDIA_TYPE_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[self::MEDIA_TYPE_OTHER])) { + $media[self::MEDIA_TYPE_OTHER] += $c; + } else { + $media[self::MEDIA_TYPE_OTHER] = $c; + } + } + + asort($media); + + return $media; + } + + /** + * @inheritDoc + */ + public function chartMedia(string $size = null, string $color_from = null, string $color_to = null): string + { + $tot = $this->totalMediaTypeQuery(self::MEDIA_TYPE_ALL); + $media = $this->getSortedMediaTypeList($tot); + + return (new ChartMedia()) + ->chartMedia($tot, $media, $size, $color_from, $color_to); + } +} diff --git a/app/Statistics/Repository/MessageRepository.php b/app/Statistics/Repository/MessageRepository.php new file mode 100644 index 0000000000..da578b8189 --- /dev/null +++ b/app/Statistics/Repository/MessageRepository.php @@ -0,0 +1,41 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\MessageRepositoryInterface; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * A repository providing methods for user message related statistics. + */ +class MessageRepository implements MessageRepositoryInterface +{ + /** + * @inheritDoc + */ + public function totalUserMessages(): string + { + $total = DB::table('message') + ->where('user_id', '=', Auth::id()) + ->count(); + + return I18N::number($total); + } +} diff --git a/app/Statistics/Repository/NewsRepository.php b/app/Statistics/Repository/NewsRepository.php new file mode 100644 index 0000000000..f570d8d519 --- /dev/null +++ b/app/Statistics/Repository/NewsRepository.php @@ -0,0 +1,69 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\NewsRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; + +/** + * A repository providing methods for news related statistics. + */ +class NewsRepository implements NewsRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * @inheritDoc + */ + public function totalUserJournal(): string + { + $number = DB::table('news') + ->where('user_id', '=', Auth::id()) + ->count(); + + return I18N::number($number); + } + + /** + * @inheritDoc + */ + public function totalGedcomNews(): string + { + $number = DB::table('news') + ->where('gedcom_id', '=', $this->tree->id()) + ->count(); + + return I18N::number($number); + } +} diff --git a/app/Statistics/Repository/PlaceRepository.php b/app/Statistics/Repository/PlaceRepository.php new file mode 100644 index 0000000000..b37732913c --- /dev/null +++ b/app/Statistics/Repository/PlaceRepository.php @@ -0,0 +1,357 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Gedcom; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Place; +use Fisharebest\Webtrees\Statistics\Google\ChartDistribution; +use Fisharebest\Webtrees\Statistics\Helper\Country; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\PlaceRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Illuminate\Database\Capsule\Manager as DB; +use Illuminate\Database\Query\JoinClause; + +/** + * A repository providing methods for place related statistics. + */ +class PlaceRepository implements PlaceRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * @var Country + */ + private $countryHelper; + + /** + * BirthPlaces constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + $this->countryHelper = new Country(); + } + + /** + * Places + * + * @param string $fact + * @param string $what + * @param bool $country + * + * @return int[] + */ + private function queryFactPlaces(string $fact, string $what = 'ALL', bool $country = false): array + { + $rows = []; + + if ($what === 'INDI') { + $rows = DB::table('individuals')->select(['i_gedcom as ged'])->where( + 'i_file', + '=', + $this->tree->id() + )->where( + 'i_gedcom', + 'LIKE', + "%\n2 PLAC %" + )->get()->all(); + } elseif ($what === 'FAM') { + $rows = DB::table('families')->select(['f_gedcom as ged'])->where( + 'f_file', + '=', + $this->tree->id() + )->where( + 'f_gedcom', + 'LIKE', + "%\n2 PLAC %" + )->get()->all(); + } + + $placelist = []; + + foreach ($rows as $row) { + if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->ged, $match)) { + if ($country) { + $tmp = explode(Gedcom::PLACE_SEPARATOR, $match[1]); + $place = end($tmp); + } else { + $place = $match[1]; + } + + if (isset($placelist[$place])) { + ++$placelist[$place]; + } else { + $placelist[$place] = 1; + } + } + } + + return $placelist; + } + + /** + * Query places. + * + * @param string $what + * @param string $fact + * @param int $parent + * @param bool $country + * + * @return int[]|\stdClass[] + */ + public function statsPlaces(string $what = 'ALL', string $fact = '', int $parent = 0, bool $country = false): array + { + if ($fact) { + return $this->queryFactPlaces($fact, $what, $country); + } + + $query = DB::table('places') + ->join('placelinks', function (JoinClause $join) { + $join->on('pl_file', '=', 'p_file') + ->on('pl_p_id', '=', 'p_id'); + }) + ->where('p_file', '=', $this->tree->id()); + + if ($parent > 0) { + // Used by placehierarchy map modules + $query->select(['p_place AS place']) + ->selectRaw('COUNT(*) AS tot') + ->where('p_id', '=', $parent) + ->groupBy(['place']); + } else { + $query->select(['p_place AS country']) + ->selectRaw('COUNT(*) AS tot') + ->where('p_parent_id', '=', 0) + ->groupBy(['country']) + ->orderByDesc('tot') + ->orderBy('country'); + } + + if ($what === 'INDI') { + $query->join('individuals', function (JoinClause $join) { + $join->on('pl_file', '=', 'i_file') + ->on('pl_gid', '=', 'i_id'); + }); + } elseif ($what === 'FAM') { + $query->join('families', function (JoinClause $join) { + $join->on('pl_file', '=', 'f_file') + ->on('pl_gid', '=', 'f_id'); + }); + } + + return $query->get()->all(); + } + + /** + * Get the top 10 places list. + * + * @param array $places + * + * @return array + */ + private function getTop10Places(array $places): array + { + $top10 = []; + $i = 0; + + arsort($places); + + foreach ($places as $place => $count) { + $tmp = new Place($place, $this->tree); + $top10[] = [ + 'place' => $tmp, + 'count' => $count, + ]; + + ++$i; + + if ($i === 10) { + break; + } + } + + return $top10; + } + + /** + * Renders the top 10 places list. + * + * @param array $places + * + * @return string + */ + private function renderTop10(array $places): string + { + $top10Records = $this->getTop10Places($places); + + return view( + 'statistics/other/top10-list', + [ + 'records' => $top10Records, + ] + ); + } + + /** + * A list of common birth places. + * + * @return string + */ + public function commonBirthPlacesList(): string + { + $places = $this->queryFactPlaces('BIRT', 'INDI'); + return $this->renderTop10($places); + } + + /** + * A list of common death places. + * + * @return string + */ + public function commonDeathPlacesList(): string + { + $places = $this->queryFactPlaces('DEAT','INDI'); + return $this->renderTop10($places); + } + + /** + * A list of common marriage places. + * + * @return string + */ + public function commonMarriagePlacesList(): string + { + $places = $this->queryFactPlaces('MARR', 'FAM'); + return $this->renderTop10($places); + } + + /** + * A list of common countries. + * + * @return string + */ + public function commonCountriesList(): string + { + $countries = $this->statsPlaces(); + + if (empty($countries)) { + return ''; + } + + $top10 = []; + $i = 1; + + // Get the country names for each language + $country_names = []; + foreach (I18N::activeLocales() as $locale) { + I18N::init($locale->languageTag()); + $all_countries = $this->countryHelper->getAllCountries(); + foreach ($all_countries as $country_code => $country_name) { + $country_names[$country_name] = $country_code; + } + } + + I18N::init(WT_LOCALE); + + $all_db_countries = []; + 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->countryHelper->getAllCountries(); + + foreach ($all_db_countries as $country_code => $country) { + foreach ($country as $country_name => $tot) { + $tmp = new Place($country_name, $this->tree); + + $top10[] = [ + 'place' => $tmp, + 'count' => $tot, + 'name' => $all_countries[$country_code], + ]; + } + + if ($i++ === 10) { + break; + } + } + + return view( + 'statistics/other/top10-list', + [ + 'records' => $top10, + ] + ); + } + + /** + * Count total places. + * + * @return int + */ + private function totalPlacesQuery(): int + { + return DB::table('places') + ->where('p_file', '=', $this->tree->id()) + ->count(); + } + + /** + * Count total places. + * + * @return string + */ + public function totalPlaces(): string + { + return I18N::number($this->totalPlacesQuery()); + } + + /** + * Create a chart showing where events occurred. + * + * @param string $chart_shows + * @param string $chart_type + * @param string $surname + * + * @return string + */ + public function chartDistribution( + string $chart_shows = 'world', + string $chart_type = '', + string $surname = '' + ): string { + $tot_pl = $this->totalPlacesQuery(); + + return (new ChartDistribution($this->tree)) + ->chartDistribution($tot_pl, $chart_shows, $chart_type, $surname); + } +} diff --git a/app/Statistics/Repository/ServerRepository.php b/app/Statistics/Repository/ServerRepository.php new file mode 100644 index 0000000000..7547db1f76 --- /dev/null +++ b/app/Statistics/Repository/ServerRepository.php @@ -0,0 +1,60 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Functions\FunctionsDate; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\ServerRepositoryInterface; + +/** + * A repository providing methods for server related statistics. + */ +class ServerRepository implements ServerRepositoryInterface +{ + /** + * @inheritDoc + */ + public function serverDate(): string + { + // TODO: Duplicates BrowserRepository::browserDate + return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display(); + } + + /** + * @inheritDoc + */ + public function serverTime(): string + { + return date('g:i a'); + } + + /** + * @inheritDoc + */ + public function serverTime24(): string + { + return date('G:i'); + } + + /** + * @inheritDoc + */ + public function serverTimezone(): string + { + return date('T'); + } +} diff --git a/app/Statistics/Repository/UserRepository.php b/app/Statistics/Repository/UserRepository.php new file mode 100644 index 0000000000..e23c9ee00e --- /dev/null +++ b/app/Statistics/Repository/UserRepository.php @@ -0,0 +1,266 @@ +<?php +/** + * webtrees: online genealogy + * Copyright (C) 2018 webtrees development team + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +declare(strict_types=1); + +namespace Fisharebest\Webtrees\Statistics\Repository; + +use Fisharebest\Webtrees\Auth; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\UserRepositoryInterface; +use Fisharebest\Webtrees\Tree; +use Fisharebest\Webtrees\User; + +/** + * A repository providing methods for user related statistics. + */ +class UserRepository implements UserRepositoryInterface +{ + /** + * @var Tree + */ + private $tree; + + /** + * Constructor. + * + * @param Tree $tree + */ + public function __construct(Tree $tree) + { + $this->tree = $tree; + } + + /** + * Who is currently logged in? + * + * @TODO - this is duplicated from the LoggedInUsersModule class. + * + * @param string $type + * + * @return string + */ + private function usersLoggedInQuery($type = 'nolist'): string + { + $content = ''; + + // List active users + $NumAnonymous = 0; + $loggedusers = []; + + 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 .= '<b>' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $NumAnonymous, I18N::number($NumAnonymous)) . '</b>'; + } + + if ($LoginUsers > 0) { + if ($NumAnonymous) { + if ($type === 'list') { + $content .= '<br><br>'; + } else { + $content .= ' ' . I18N::translate('and') . ' '; + } + } + $content .= '<b>' . I18N::plural('%s signed-in user', '%s signed-in users', $LoginUsers, I18N::number($LoginUsers)) . '</b>'; + if ($type === 'list') { + $content .= '<ul>'; + } else { + $content .= ': '; + } + } + + if (Auth::check()) { + foreach ($loggedusers as $user) { + if ($type === 'list') { + $content .= '<li>' . e($user->getRealName()) . ' - ' . e($user->getUserName()); + } else { + $content .= e($user->getRealName()) . ' - ' . e($user->getUserName()); + } + + if (($user->getPreference('contactmethod') !== 'none') + && (Auth::id() !== $user->id()) + ) { + if ($type === 'list') { + $content .= '<br>'; + } + $content .= '<a href="' . e(route('message', ['to' => $user->getUserName(), 'ged' => $this->tree->name()])) . '" class="btn btn-link" title="' . I18N::translate('Send a message') . '">' . view('icons/email') . '</a>'; + } + + if ($type === 'list') { + $content .= '</li>'; + } + } + } + + if ($type === 'list') { + $content .= '</ul>'; + } + + return $content; + } + + /** + * @inheritDoc + */ + public function usersLoggedIn(): string + { + return $this->usersLoggedInQuery(); + } + + /** + * @inheritDoc + */ + public function usersLoggedInList(): string + { + return $this->usersLoggedInQuery('list'); + } + + /** + * Returns true if the given user is visible to others. + * + * @param User $user + * + * @return bool + */ + private function isUserVisible(User $user): bool + { + return Auth::isAdmin() || $user->getPreference('visibleonline'); + } + + /** + * @inheritDoc + */ + public function usersLoggedInTotal(): int + { + return \count(User::allLoggedIn()); + } + + /** + * @inheritDoc + */ + public function usersLoggedInTotalAnon(): int + { + $anonymous = 0; + + foreach (User::allLoggedIn() as $user) { + if (!$this->isUserVisible($user)) { + ++$anonymous; + } + } + + return $anonymous; + } + + /** + * @inheritDoc + */ + public function usersLoggedInTotalVisible(): int + { + $visible = 0; + + foreach (User::allLoggedIn() as $user) { + if ($this->isUserVisible($user)) { + ++$visible; + } + } + + return $visible; + } + + /** + * @inheritDoc + */ + public function userId(): string + { + return (string) Auth::id(); + } + + /** + * @inheritDoc + */ + public function userName(string $visitor_text = ''): string + { + if (Auth::check()) { + return e(Auth::user()->getUserName()); + } + + // if #username:visitor# was specified, then "visitor" will be returned when the user is not logged in + return e($visitor_text); + } + + /** + * @inheritDoc + */ + public function userFullName(): string + { + return Auth::check() ? '<span dir="auto">' . e(Auth::user()->getRealName()) . '</span>' : ''; + } + + /** + * Returns the user count. + * + * @return int + */ + private function getUserCount(): int + { + return \count(User::all()); + } + + /** + * Returns the administrator count. + * + * @return int + */ + private function getAdminCount(): int + { + return \count(User::administrators()); + } + + /** + * @inheritDoc + */ + public function totalUsers(): string + { + return I18N::number($this->getUserCount()); + } + + /** + * @inheritDoc + */ + public function totalAdmins(): string + { + return I18N::number($this->getAdminCount()); + } + + /** + * @inheritDoc + */ + public function totalNonAdmins(): string + { + return I18N::number($this->getUserCount() - $this->getAdminCount()); + } +} diff --git a/app/Stats.php b/app/Stats.php index 99b6f527b7..fc635062b9 100644 --- a/app/Stats.php +++ b/app/Stats.php @@ -17,35 +17,76 @@ declare(strict_types=1); namespace Fisharebest\Webtrees; -use Fisharebest\Webtrees\Functions\FunctionsDate; -use Fisharebest\Webtrees\Functions\FunctionsPrint; -use Fisharebest\Webtrees\Functions\FunctionsPrintLists; -use Fisharebest\Webtrees\Module\FamilyTreeFavoritesModule; -use Fisharebest\Webtrees\Module\HitCountFooterModule; use Fisharebest\Webtrees\Module\ModuleBlockInterface; use Fisharebest\Webtrees\Module\ModuleInterface; -use Fisharebest\Webtrees\Module\UserFavoritesModule; -use Illuminate\Database\Capsule\Manager as DB; -use PDOException; -use stdClass; -use Symfony\Component\HttpFoundation\Request; -use const PREG_SET_ORDER; +use Fisharebest\Webtrees\Statistics\Repository\BrowserRepository; +use Fisharebest\Webtrees\Statistics\Repository\ContactRepository; +use Fisharebest\Webtrees\Statistics\Repository\EventRepository; +use Fisharebest\Webtrees\Statistics\Repository\FamilyDatesRepository; +use Fisharebest\Webtrees\Statistics\Repository\FamilyRepository; +use Fisharebest\Webtrees\Statistics\Repository\FavoritesRepository; +use Fisharebest\Webtrees\Statistics\Repository\GedcomRepository; +use Fisharebest\Webtrees\Statistics\Repository\HitCountRepository; +use Fisharebest\Webtrees\Statistics\Repository\IndividualRepository; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\BrowserRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\ContactRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\EventRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\FamilyDatesRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\FavoritesRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\GedcomRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\HitCountRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\LatestUserRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\MediaRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\MessageRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\NewsRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\PlaceRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\ServerRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\Interfaces\UserRepositoryInterface; +use Fisharebest\Webtrees\Statistics\Repository\LatestUserRepository; +use Fisharebest\Webtrees\Statistics\Repository\MediaRepository; +use Fisharebest\Webtrees\Statistics\Repository\MessageRepository; +use Fisharebest\Webtrees\Statistics\Repository\NewsRepository; +use Fisharebest\Webtrees\Statistics\Repository\PlaceRepository; +use Fisharebest\Webtrees\Statistics\Repository\ServerRepository; +use Fisharebest\Webtrees\Statistics\Repository\UserRepository; +use ReflectionMethod; /** * 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 +class Stats implements + GedcomRepositoryInterface, + IndividualRepositoryInterface, + EventRepositoryInterface, + MediaRepositoryInterface, + UserRepositoryInterface, + ServerRepositoryInterface, + BrowserRepositoryInterface, + HitCountRepositoryInterface, + LatestUserRepositoryInterface, + FavoritesRepositoryInterface, + NewsRepositoryInterface, + MessageRepositoryInterface, + ContactRepositoryInterface, + FamilyDatesRepositoryInterface, + PlaceRepositoryInterface { - // Used in Google charts - const GOOGLE_CHART_ENCODING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.'; - - /** @var Tree Generate statistics for a specified tree. */ + /** + * Generate statistics for a specified tree. + * + * @var Tree + */ private $tree; - /** @var string[] All public functions are available as keywords - except these ones */ - private $public_but_not_allowed = [ + /** + * All public functions are available as keywords - except these ones + * + * @var string[] + */ + private static $public_but_not_allowed = [ '__construct', 'embedTags', 'iso3166', @@ -62,27 +103,85 @@ class Stats 'statsMarrAgeQuery', ]; - /** @var string[] List of GEDCOM media types */ - private $media_types = [ - 'audio', - 'book', - 'card', - 'certificate', - 'coat', - 'document', - 'electronic', - 'magazine', - 'manuscript', - 'map', - 'fiche', - 'film', - 'newspaper', - 'painting', - 'photo', - 'tombstone', - 'video', - 'other', - ]; + /** + * @var GedcomRepository + */ + private $gedcomRepository; + + /** + * @var IndividualRepository + */ + private $individualRepository; + + /** + * @var FamilyRepository + */ + private $familyRepository; + + /** + * @var MediaRepository + */ + private $mediaRepository; + + /** + * @var EventRepository + */ + private $eventRepository; + + /** + * @var UserRepository + */ + private $userRepository; + + /** + * @var ServerRepository + */ + private $serverRepository; + + /** + * @var BrowserRepository + */ + private $browserRepository; + + /** + * @var HitCountRepository + */ + private $hitCountRepository; + + /** + * @var LatestUserRepository + */ + private $latestUserRepository; + + /** + * @var FavoritesRepository + */ + private $favoritesRepository; + + /** + * @var NewsRepository + */ + private $newsRepository; + + /** + * @var MessageRepository + */ + private $messageRepository; + + /** + * @var ContactRepository + */ + private $contactRepository; + + /** + * @var FamilyDatesRepository + */ + private $familyDatesRepository; + + /** + * @var PlaceRepository + */ + private $placeRepository; /** * Create the statistics for a tree. @@ -91,7 +190,23 @@ class Stats */ public function __construct(Tree $tree) { - $this->tree = $tree; + $this->tree = $tree; + $this->gedcomRepository = new GedcomRepository($tree); + $this->individualRepository = new IndividualRepository($tree); + $this->familyRepository = new FamilyRepository($tree); + $this->familyDatesRepository = new FamilyDatesRepository($tree); + $this->mediaRepository = new MediaRepository($tree); + $this->eventRepository = new EventRepository($tree); + $this->userRepository = new UserRepository($tree); + $this->serverRepository = new ServerRepository(); + $this->browserRepository = new BrowserRepository(); + $this->hitCountRepository = new HitCountRepository($tree); + $this->latestUserRepository = new LatestUserRepository(); + $this->favoritesRepository = new FavoritesRepository($tree); + $this->newsRepository = new NewsRepository($tree); + $this->messageRepository = new MessageRepository(); + $this->contactRepository = new ContactRepository($tree); + $this->placeRepository = new PlaceRepository($tree); } /** @@ -102,12 +217,14 @@ class Stats public function getAllTagsTable(): string { $examples = []; + foreach (get_class_methods($this) as $method) { - $reflection = new \ReflectionMethod($this, $method); - if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) { + $reflection = new ReflectionMethod($this, $method); + if ($reflection->isPublic() && !\in_array($method, self::$public_but_not_allowed, true)) { $examples[$method] = $this->$method(); } } + ksort($examples); $html = ''; @@ -127,12 +244,14 @@ class Stats public function getAllTagsText(): string { $examples = []; + foreach (get_class_methods($this) as $method) { - $reflection = new \ReflectionMethod($this, $method); - if ($reflection->isPublic() && !in_array($method, $this->public_but_not_allowed)) { + $reflection = new ReflectionMethod($this, $method); + if ($reflection->isPublic() && !\in_array($method, self::$public_but_not_allowed, true)) { $examples[$method] = $method; } } + ksort($examples); return implode('<br>', $examples); @@ -147,7 +266,8 @@ class Stats */ private function getTags(string $text): array { - $tags = []; + $tags = []; + $matches = []; preg_match_all('/#([^#]+)#/', $text, $matches, PREG_SET_ORDER); @@ -180,6234 +300,2116 @@ class Stats } /** - * Get the name used for GEDCOM files and URLs. - * - * @return string + * @inheritDoc */ public function gedcomFilename(): string { - return $this->tree->name(); + return $this->gedcomRepository->gedcomFilename(); } /** - * Get the internal ID number of the tree. - * - * @return int + * @inheritDoc */ public function gedcomId(): int { - return $this->tree->id(); + return $this->gedcomRepository->gedcomId(); } /** - * Get the descriptive title of the tree. - * - * @return string + * @inheritDoc */ public function gedcomTitle(): string { - return e($this->tree->title()); - } - - /** - * Get information from the GEDCOM's HEAD record. - * - * @return string[] - */ - private function gedcomHead(): array - { - $title = ''; - $version = ''; - $source = ''; - - $head = GedcomRecord::getInstance('HEAD', $this->tree); - $sour = $head->getFirstFact('SOUR'); - if ($sour !== null) { - $source = $sour->value(); - $title = $sour->attribute('NAME'); - $version = $sour->attribute('VERS'); - } - - return [ - $title, - $version, - $source, - ]; + return $this->gedcomRepository->gedcomTitle(); } /** - * Get the software originally used to create the GEDCOM file. - * - * @return string + * @inheritDoc */ public function gedcomCreatedSoftware(): string { - $head = $this->gedcomHead(); - - return $head[0]; + return $this->gedcomRepository->gedcomCreatedSoftware(); } /** - * Get the version of software which created the GEDCOM file. - * - * @return string + * @inheritDoc */ public function gedcomCreatedVersion(): string { - $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]; + return $this->gedcomRepository->gedcomCreatedVersion(); } /** - * Get the date the GEDCOM file was created. - * - * @return string + * @inheritDoc */ public function gedcomDate(): string { - $head = GedcomRecord::getInstance('HEAD', $this->tree); - $fact = $head->getFirstFact('DATE'); - if ($fact) { - $date = new Date($fact->value()); - - return $date->display(); - } - - return ''; + return $this->gedcomRepository->gedcomDate(); } /** - * When was this tree last updated? - * - * @return string + * @inheritDoc */ public function gedcomUpdated(): string { - $row = Database::prepare( - "SELECT 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([$this->tree->id()])->fetchOneRow(); - if ($row) { - $date = new Date("{$row->d_day} {$row->d_month} {$row->d_year}"); - - return $date->display(); - } - - return $this->gedcomDate(); + return $this->gedcomRepository->gedcomUpdated(); } /** - * What is the significant individual from this tree? - * - * @return string + * @inheritDoc */ public function gedcomRootId(): string { - return $this->tree->getPreference('PEDIGREE_ROOT_ID'); - } - - /** - * Convert totals into percentages. - * - * @param int $total - * @param string $type - * - * @return string - */ - private function getPercentage(int $total, string $type): string - { - 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; - } - - return I18N::percentage($total / $type, 1); + return $this->gedcomRepository->gedcomRootId(); } /** - * How many GEDCOM records exist in the tree. - * - * @return string + * @inheritDoc */ public function totalRecords(): string { - return I18N::number($this->totalIndividualsQuery() + $this->totalFamiliesQuery() + $this->totalSourcesQuery()); + return $this->individualRepository->totalRecords(); } /** - * How many individuals exist in the tree. - * - * @return int - */ - private function totalIndividualsQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * How many individuals exist in the tree. - * - * @return string + * @inheritDoc */ public function totalIndividuals(): string { - return I18N::number($this->totalIndividualsQuery()); - } - - /** - * How many individuals have one or more sources. - * - * @return int - */ - private function totalIndisWithSourcesQuery(): int - { - return (int) Database::prepare( - "SELECT 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([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); + return $this->individualRepository->totalIndividuals(); } /** - * How many individuals have one or more sources. - * - * @return string + * @inheritDoc */ public function totalIndisWithSources(): string { - return I18N::number($this->totalIndisWithSourcesQuery()); + return $this->individualRepository->totalIndisWithSources(); } /** - * Create a chart showing individuals with/without sources. - * - * @param string|null $size // Optional parameter, set from tag - * @param string|null $color_from - * @param string|null $color_to - * - * @return string + * @inheritDoc */ - public function chartIndisWithSources(string $size = null, string $color_from = null, string $color_to = null): string - { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - - $sizes = explode('x', $size); - $tot_indi = $this->totalIndividualsQuery(); - if ($tot_indi == 0) { - return ''; - } - - $tot_sindi_per = $this->totalIndisWithSourcesQuery() / $tot_indi; - $with = (int) (100 * $tot_sindi_per); - $chd = $this->arrayToExtendedEncoding([100 - $with, $with]); - $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 '<img src="https://chart.googleapis.com/chart?cht=p3&chd=e:' . $chd . '&chs=' . $size . '&chco=' . $color_from . ',' . $color_to . '&chf=bg,s,ffffff00&chl=' . rawurlencode($chl) . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . $chart_title . '" title="' . $chart_title . '">'; + public function chartIndisWithSources( + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + return $this->individualRepository->chartIndisWithSources($size, $color_from, $color_to); } /** - * Show the total individuals as a percentage. - * - * @return string + * @inheritDoc */ public function totalIndividualsPercentage(): string { - return $this->getPercentage($this->totalIndividualsQuery(), 'all'); + return $this->individualRepository->totalIndividualsPercentage(); } /** - * Count the total families. - * - * @return int - */ - private function totalFamiliesQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##families` WHERE f_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the total families. - * - * @return string + * @inheritDoc */ public function totalFamilies(): string { - return I18N::number($this->totalFamiliesQuery()); + return $this->individualRepository->totalFamilies(); } /** - * Count the families with source records. - * - * @return int + * @inheritDoc */ - private function totalFamsWithSourcesQuery(): int + public function totalFamiliesPercentage(): string { - return (int) Database::prepare( - "SELECT 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([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); + return $this->individualRepository->totalFamiliesPercentage(); } /** - * Count the families with with source records. - * - * @return string + * @inheritDoc */ public function totalFamsWithSources(): string { - return I18N::number($this->totalFamsWithSourcesQuery()); + return $this->individualRepository->totalFamsWithSources(); } /** - * Create a chart of individuals with/without sources. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string + * @inheritDoc */ - public function chartFamsWithSources(string $size = null, string $color_from = null, string $color_to = null): string - { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - - $sizes = explode('x', $size); - $tot_fam = $this->totalFamiliesQuery(); - if ($tot_fam == 0) { - return ''; - } - - $tot_sfam_per = $this->totalFamsWithSourcesQuery() / $tot_fam; - $with = (int) (100 * $tot_sfam_per); - $chd = $this->arrayToExtendedEncoding([100 - $with, $with]); - $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 "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . '" title="' . $chart_title . '" />'; - } - - /** - * Show the total families as a percentage. - * - * @return string - */ - public function totalFamiliesPercentage(): string - { - return $this->getPercentage($this->totalFamiliesQuery(), 'all'); - } - - /** - * Count the total number of sources. - * - * @return int - */ - private function totalSourcesQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##sources` WHERE s_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); + public function chartFamsWithSources( + string $size = null, + string $color_from = null, + string $color_to = null + ): string { + return $this->individualRepository->chartFamsWithSources($size, $color_from, $color_to); } /** - * Count the total number of sources. - * - * @return string + * @inheritDoc */ public function totalSources(): string { - return I18N::number($this->totalSourcesQuery()); + return $this->individualRepository->totalSources(); } /** - * Show the number of sources as a percentage. - * - * @return string + * @inheritDoc */ public function totalSourcesPercentage(): string { - return $this->getPercentage($this->totalSourcesQuery(), 'all'); + return $this->individualRepository->totalSourcesPercentage(); } /** - * Count the number of notes. - * - * @return int - */ - private function totalNotesQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##other` WHERE o_type='NOTE' AND o_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of notes. - * - * @return string + * @inheritDoc */ public function totalNotes(): string { - return I18N::number($this->totalNotesQuery()); + return $this->individualRepository->totalNotes(); } /** - * Show the number of notes as a percentage. - * - * @return string + * @inheritDoc */ public function totalNotesPercentage(): string { - return $this->getPercentage($this->totalNotesQuery(), 'all'); + return $this->individualRepository->totalNotesPercentage(); } /** - * Count the number of repositories. - * - * @return int - */ - private function totalRepositoriesQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##other` WHERE o_type='REPO' AND o_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of repositories - * - * @return string + * @inheritDoc */ public function totalRepositories(): string { - return I18N::number($this->totalRepositoriesQuery()); + return $this->individualRepository->totalRepositories(); } /** - * Show the total number of repositories as a percentage. - * - * @return string + * @inheritDoc */ public function totalRepositoriesPercentage(): string { - return $this->getPercentage($this->totalRepositoriesQuery(), 'all'); + return $this->individualRepository->totalRepositoriesPercentage(); } /** - * Count the surnames. - * - * @param string ...$params - * - * @return string + * @inheritDoc */ public function totalSurnames(...$params): string { - if ($params) { - $opt = 'IN (' . implode(',', array_fill(0, count($params), '?')) . ')'; - $distinct = ''; - } else { - $opt = "IS NOT NULL"; - $distinct = 'DISTINCT'; - } - $params[] = $this->tree->id(); - - $total = (int) Database::prepare( - "SELECT 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); + return $this->individualRepository->totalSurnames(...$params); } /** - * Count the number of distinct given names, or count the number of - * occurrences of a specific name or names. - * - * @param string ...$params - * - * @return string + * @inheritDoc */ public function totalGivennames(...$params): string { - if ($params) { - $qs = implode(',', array_fill(0, count($params), '?')); - $params[] = $this->tree->id(); - $total = (int) Database::prepare( - "SELECT COUNT( n_givn) FROM `##name` WHERE n_givn IN ({$qs}) AND n_file=?" - )->execute( - $params - )->fetchOne(); - } else { - $total = (int) Database::prepare( - "SELECT COUNT(DISTINCT n_givn) FROM `##name` WHERE n_givn IS NOT NULL AND n_file=?" - )->execute([ - $this->tree->id(), - ])->fetchOne(); - } - - return I18N::number($total); + return $this->individualRepository->totalGivennames(...$params); } /** - * Count the number of events (with dates). - * - * @param string[] $events - * - * @return string + * @inheritDoc */ public function totalEvents(array $events = []): string { - $sql = "SELECT COUNT(*) AS tot FROM `##dates` WHERE d_file=?"; - $vars = [$this->tree->id()]; - - $no_types = [ - 'HEAD', - 'CHAN', - ]; - if ($events) { - $types = []; - foreach ($events 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); - - $n = (int) Database::prepare($sql)->execute($vars)->fetchOne(); - - return I18N::number($n); + return $this->eventRepository->totalEvents($events); } /** - * Count the number of births. - * - * @return string + * @inheritDoc */ public function totalEventsBirth(): string { - return $this->totalEvents(Gedcom::BIRTH_EVENTS); + return $this->eventRepository->totalEventsBirth(); } /** - * Count the number of births. - * - * @return string + * @inheritDoc */ public function totalBirths(): string { - return $this->totalEvents(['BIRT']); + return $this->eventRepository->totalBirths(); } /** - * Count the number of deaths. - * - * @return string + * @inheritDoc */ public function totalEventsDeath(): string { - return $this->totalEvents(Gedcom::DEATH_EVENTS); + return $this->eventRepository->totalEventsDeath(); } /** - * Count the number of deaths. - * - * @return string + * @inheritDoc */ public function totalDeaths(): string { - return $this->totalEvents(['DEAT']); + return $this->eventRepository->totalDeaths(); } /** - * Count the number of marriages. - * - * @return string + * @inheritDoc */ public function totalEventsMarriage(): string { - return $this->totalEvents(Gedcom::MARRIAGE_EVENTS); + return $this->eventRepository->totalEventsMarriage(); } /** - * Count the number of marriages. - * - * @return string + * @inheritDoc */ public function totalMarriages(): string { - return $this->totalEvents(['MARR']); + return $this->eventRepository->totalMarriages(); } /** - * Count the number of divorces. - * - * @return string + * @inheritDoc */ public function totalEventsDivorce(): string { - return $this->totalEvents(Gedcom::DIVORCE_EVENTS); + return $this->eventRepository->totalEventsDivorce(); } /** - * Count the number of divorces. - * - * @return string + * @inheritDoc */ public function totalDivorces(): string { - return $this->totalEvents(['DIV']); + return $this->eventRepository->totalDivorces(); } /** - * Count the number of other events. - * - * @return string + * @inheritDoc */ public function totalEventsOther(): string { - $facts = array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS); - $no_facts = []; - foreach ($facts as $fact) { - $fact = '!' . str_replace('\'', '', $fact); - $no_facts[] = $fact; - } - - return $this->totalEvents($no_facts); + return $this->eventRepository->totalEventsOther(); } /** - * Count the number of males. - * - * @return int - */ - private function totalSexMalesQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'M'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of males. - * - * @return string + * @inheritDoc */ public function totalSexMales(): string { - return I18N::number($this->totalSexMalesQuery()); + return $this->individualRepository->totalSexMales(); } /** - * Count the number of males - * - * @return string + * @inheritDoc */ public function totalSexMalesPercentage(): string { - return $this->getPercentage($this->totalSexMalesQuery(), 'individual'); + return $this->individualRepository->totalSexMalesPercentage(); } /** - * Count the number of females. - * - * @return int - */ - private function totalSexFemalesQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'F'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of females. - * - * @return string + * @inheritDoc */ public function totalSexFemales(): string { - return I18N::number($this->totalSexFemalesQuery()); + return $this->individualRepository->totalSexFemales(); } /** - * Count the number of females. - * - * @return string + * @inheritDoc */ public function totalSexFemalesPercentage(): string { - return $this->getPercentage($this->totalSexFemalesQuery(), 'individual'); + return $this->individualRepository->totalSexFemalesPercentage(); } /** - * Count the number of individuals with unknown sex. - * - * @return int - */ - private function totalSexUnknownQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_sex = 'U'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of individuals with unknown sex. - * - * @return string + * @inheritDoc */ public function totalSexUnknown(): string { - return I18N::number($this->totalSexUnknownQuery()); + return $this->individualRepository->totalSexUnknown(); } /** - * Count the number of individuals with unknown sex. - * - * @return string + * @inheritDoc */ public function totalSexUnknownPercentage(): string { - return $this->getPercentage($this->totalSexUnknownQuery(), 'individual'); + return $this->individualRepository->totalSexUnknownPercentage(); } /** - * Generate a chart showing sex distribution. - * - * @param string|null $size - * @param string|null $color_female - * @param string|null $color_male - * @param string|null $color_unknown - * - * @return string + * @inheritDoc */ - public function chartSex(string $size = null, string $color_female = null, string $color_male = null, string $color_unknown = null): string - { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_female = $color_female ?? 'ffd1dc'; - $color_male = $color_male ?? '84beff'; - $color_unknown = $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 ''; - } - - if ($tot_u > 0) { - $chd = $this->arrayToExtendedEncoding([ - intdiv(4095 * $tot_u, $tot), - intdiv(4095 * $tot_f, $tot), - intdiv(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 "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_unknown},{$color_female},{$color_male}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . '" title="' . $chart_title . '" />'; - } - - $chd = $this->arrayToExtendedEncoding([ - intdiv(4095 * $tot_f, $tot), - intdiv(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 "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_female},{$color_male}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . '" title="' . $chart_title . '" />'; + public function chartSex( + string $size = null, + string $color_female = null, + string $color_male = null, + string $color_unknown = null + ): string { + return $this->individualRepository->chartSex($size, $color_female, $color_male, $color_unknown); } /** - * 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(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom NOT REGEXP '\\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ")'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of living individuals. - * - * @return string + * @inheritDoc */ public function totalLiving(): string { - return I18N::number($this->totalLivingQuery()); + return $this->individualRepository->totalLiving(); } /** - * Count the number of living individuals. - * - * @return string + * @inheritDoc */ public function totalLivingPercentage(): string { - return $this->getPercentage($this->totalLivingQuery(), 'individual'); + return $this->individualRepository->totalLivingPercentage(); } /** - * Count the number of dead individuals. - * - * @return int - */ - private function totalDeceasedQuery(): int - { - return (int) Database::prepare( - "SELECT COUNT(*) FROM `##individuals` WHERE i_file = :tree_id AND i_gedcom REGEXP '\\n1 (" . implode('|', Gedcom::DEATH_EVENTS) . ")'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - } - - /** - * Count the number of dead individuals. - * - * @return string + * @inheritDoc */ public function totalDeceased(): string { - return I18N::number($this->totalDeceasedQuery()); + return $this->individualRepository->totalDeceased(); } /** - * Count the number of dead individuals. - * - * @return string + * @inheritDoc */ public function totalDeceasedPercentage(): string { - return $this->getPercentage($this->totalDeceasedQuery(), 'individual'); + return $this->individualRepository->totalDeceasedPercentage(); } /** - * Create a chart showing mortality. - * - * @param string|null $size - * @param string|null $color_living - * @param string|null $color_dead - * - * @return string + * @inheritDoc */ public function chartMortality(string $size = null, string $color_living = null, string $color_dead = null): string { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_living = $color_living ?? 'ffffff'; - $color_dead = $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 ''; - } - - $chd = $this->arrayToExtendedEncoding([ - intdiv(4095 * $tot_l, $tot), - intdiv(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 "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_living},{$color_dead}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . '" title="' . $chart_title . '" />'; + return $this->individualRepository->chartMortality($size, $color_living, $color_dead); } /** - * Count the number of users. - * - * @return string - */ - public function totalUsers(): string - { - $total = count(User::all()); - - return I18N::number($total); - } - - /** - * Count the number of administrators. - * - * @return string - */ - public function totalAdmins(): string - { - return I18N::number(count(User::administrators())); - } - - /** - * Count the number of administrators. - * - * @return string - */ - public function totalNonAdmins(): string - { - return I18N::number(count(User::all()) - count(User::administrators())); - } - - /** - * Count the number of media records with a given type. - * - * @param string $type - * - * @return int - */ - private function totalMediaType($type): int - { - if (!in_array($type, $this->media_types) && $type != 'all' && $type != 'unknown') { - return 0; - } - $sql = "SELECT COUNT(*) AS tot FROM `##media` WHERE m_file=?"; - $vars = [$this->tree->id()]; - - 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 + * @inheritDoc */ public function totalMedia(): string { - return I18N::number($this->totalMediaType('all')); + return $this->mediaRepository->totalMedia(); } /** - * Count the number of media records with type "audio". - * - * @return string + * @inheritDoc */ public function totalMediaAudio(): string { - return I18N::number($this->totalMediaType('audio')); + return $this->mediaRepository->totalMediaAudio(); } /** - * Count the number of media records with type "book". - * - * @return string + * @inheritDoc */ public function totalMediaBook(): string { - return I18N::number($this->totalMediaType('book')); + return $this->mediaRepository->totalMediaBook(); } /** - * Count the number of media records with type "card". - * - * @return string + * @inheritDoc */ public function totalMediaCard(): string { - return I18N::number($this->totalMediaType('card')); + return $this->mediaRepository->totalMediaCard(); } /** - * Count the number of media records with type "certificate". - * - * @return string + * @inheritDoc */ public function totalMediaCertificate(): string { - return I18N::number($this->totalMediaType('certificate')); + return $this->mediaRepository->totalMediaCertificate(); } /** - * Count the number of media records with type "coat of arms". - * - * @return string + * @inheritDoc */ public function totalMediaCoatOfArms(): string { - return I18N::number($this->totalMediaType('coat')); + return $this->mediaRepository->totalMediaCoatOfArms(); } /** - * Count the number of media records with type "document". - * - * @return string + * @inheritDoc */ public function totalMediaDocument(): string { - return I18N::number($this->totalMediaType('document')); + return $this->mediaRepository->totalMediaDocument(); } /** - * Count the number of media records with type "electronic". - * - * @return string + * @inheritDoc */ public function totalMediaElectronic(): string { - return I18N::number($this->totalMediaType('electronic')); + return $this->mediaRepository->totalMediaElectronic(); } /** - * Count the number of media records with type "magazine". - * - * @return string + * @inheritDoc */ public function totalMediaMagazine(): string { - return I18N::number($this->totalMediaType('magazine')); + return $this->mediaRepository->totalMediaMagazine(); } /** - * Count the number of media records with type "manuscript". - * - * @return string + * @inheritDoc */ public function totalMediaManuscript(): string { - return I18N::number($this->totalMediaType('manuscript')); + return $this->mediaRepository->totalMediaManuscript(); } /** - * Count the number of media records with type "map". - * - * @return string + * @inheritDoc */ public function totalMediaMap(): string { - return I18N::number($this->totalMediaType('map')); + return $this->mediaRepository->totalMediaMap(); } /** - * Count the number of media records with type "microfiche". - * - * @return string + * @inheritDoc */ public function totalMediaFiche(): string { - return I18N::number($this->totalMediaType('fiche')); + return $this->mediaRepository->totalMediaFiche(); } /** - * Count the number of media records with type "microfilm". - * - * @return string + * @inheritDoc */ public function totalMediaFilm(): string { - return I18N::number($this->totalMediaType('film')); + return $this->mediaRepository->totalMediaFilm(); } /** - * Count the number of media records with type "newspaper". - * - * @return string + * @inheritDoc */ public function totalMediaNewspaper(): string { - return I18N::number($this->totalMediaType('newspaper')); + return $this->mediaRepository->totalMediaNewspaper(); } /** - * Count the number of media records with type "painting". - * - * @return string + * @inheritDoc */ public function totalMediaPainting(): string { - return I18N::number($this->totalMediaType('painting')); + return $this->mediaRepository->totalMediaPainting(); } /** - * Count the number of media records with type "photograph". - * - * @return string + * @inheritDoc */ public function totalMediaPhoto(): string { - return I18N::number($this->totalMediaType('photo')); + return $this->mediaRepository->totalMediaPhoto(); } /** - * Count the number of media records with type "tombstone". - * - * @return string + * @inheritDoc */ public function totalMediaTombstone(): string { - return I18N::number($this->totalMediaType('tombstone')); + return $this->mediaRepository->totalMediaTombstone(); } /** - * Count the number of media records with type "video". - * - * @return string + * @inheritDoc */ public function totalMediaVideo(): string { - return I18N::number($this->totalMediaType('video')); + return $this->mediaRepository->totalMediaVideo(); } /** - * Count the number of media records with type "other". - * - * @return string + * @inheritDoc */ public function totalMediaOther(): string { - return I18N::number($this->totalMediaType('other')); + return $this->mediaRepository->totalMediaOther(); } /** - * Count the number of media records with type "unknown". - * - * @return string + * @inheritDoc */ public function totalMediaUnknown(): string { - return I18N::number($this->totalMediaType('unknown')); + return $this->mediaRepository->totalMediaUnknown(); } /** - * Create a chart of media types. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string + * @inheritDoc */ public function chartMedia(string $size = null, string $color_from = null, string $color_to = null): string { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $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 = []; - $mediaTypes = ''; - $chart_title = ''; - $c = 0; - $max = 0; - $media = []; - 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[] = intdiv(100 * $count, $tot); - $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 "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . '" title="' . $chart_title . '" />'; + return $this->mediaRepository->chartMedia($size, $color_from, $color_to); } /** - * Birth and Death - * - * @param string $type - * @param string $life_dir - * @param string $birth_death - * - * @return string + * @inheritDoc */ - private function mortalityQuery($type, $life_dir, $birth_death): string + public function statsPlaces(string $what = 'ALL', string $fact = '', int $parent = 0, bool $country = false): array { - 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 d_year, d_type, d_fact, d_gid" . - " FROM `##dates`" . - " WHERE d_file={$this->tree->id()} AND d_fact IN ({$query_field}) AND d_julianday1=(" . - " SELECT {$dmod}( d_julianday1 )" . - " FROM `##dates`" . - " WHERE d_file={$this->tree->id()} 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(); - } else { - $result = I18N::translate('This information is private and cannot be shown.'); - } - break; - case 'year': - if ($row->d_year < 0) { - $row->d_year = abs($row->d_year) . ' B.C.'; - } - $date = new Date($row->d_type . ' ' . $row->d_year); - $result = $date->display(); - break; - case 'name': - $result = '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>'; - 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; + return $this->placeRepository->statsPlaces($what, $fact, $parent, $country); } /** - * Places - * - * @param string $what - * @param string $fact - * @param int $parent - * @param bool $country - * - * @return int[]|stdClass[] - */ - public function statsPlaces($what = 'ALL', $fact = '', $parent = 0, $country = false): array - { - 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([ - 'tree_id' => $this->tree->id(), - ])->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([ - 'tree_id' => $this->tree->id(), - ])->fetchAll(); - } - $placelist = []; - foreach ($rows as $row) { - if (preg_match('/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $row->ged, $match)) { - if ($country) { - $tmp = explode(Gedcom::PLACE_SEPARATOR, $match[1]); - $place = end($tmp); - } else { - $place = $match[1]; - } - if (!isset($placelist[$place])) { - $placelist[$place] = 1; - } else { - $placelist[$place]++; - } - } - } - - return $placelist; - } - - if ($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" . - " 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->id()}" . - " GROUP BY place" - ); - - return $rows; - } - - 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" . - " 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->id()}" . - " AND p_parent_id='0'" . - " GROUP BY country ORDER BY tot DESC, country ASC" - ); - - return $rows; - } - - /** - * Count total places. - * - * @return int - */ - private function totalPlacesQuery(): int - { - return - (int) Database::prepare("SELECT COUNT(*) FROM `##places` WHERE p_file=?") - ->execute([$this->tree->id()]) - ->fetchOne(); - } - - /** - * Count total places. - * - * @return string + * @inheritDoc */ public function totalPlaces(): string { - return I18N::number($this->totalPlacesQuery()); + return $this->placeRepository->totalPlaces(); } /** - * Create a chart showing where events occurred. - * - * @param string $chart_shows - * @param string $chart_type - * @param string $surname - * - * @return string + * @inheritDoc */ - public function chartDistribution(string $chart_shows = 'world', string $chart_type = '', string $surname = ''): string - { - $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 ($this->totalPlacesQuery() == 0) { - return ''; - } - // Get the country names for each language - $country_to_iso3166 = []; - 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 = []; - - $rows = Database::prepare( - "SELECT i_gedcom" . - " FROM `##individuals`" . - " JOIN `##name` ON n_id = i_id AND n_file = i_file" . - " WHERE n_file = :tree_id" . - " AND n_surn COLLATE :collate = :surname" - )->execute([ - 'tree_id' => $this->tree->id(), - 'collate' => I18N::collation(), - 'surname' => $surname, - ])->fetchAll(); - - foreach ($rows as $row) { - if (preg_match_all('/^2 PLAC (?:.*, *)*(.*)/m', $row->i_gedcom, $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 = []; - $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 = []; - $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 = []; - $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 = []; - $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(self::GOOGLE_CHART_ENCODING, (int) ($count / max($surn_countries) * 61), 1); - } - $chart = '<div id="google_charts" class="center">'; - $chart .= '<p>' . $chart_title . '</p>'; - $chart .= '<div><img src="' . $chart_url . '" alt="' . $chart_title . '" title="' . $chart_title . '" class="gchart" /><br>'; - $chart .= '<table class="center"><tr>'; - $chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR2 . '" width="12"></td><td>' . I18N::translate('Highest population') . '</td>'; - $chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR3 . '" width="12"></td><td>' . I18N::translate('Lowest population') . '</td>'; - $chart .= '<td bgcolor="#' . $WT_STATS_CHART_COLOR1 . '" width="12"></td><td>' . I18N::translate('Nobody at all') . '</td>'; - $chart .= '</tr></table></div></div>'; - - return $chart; + public function chartDistribution( + string $chart_shows = 'world', + string $chart_type = '', + string $surname = '' + ) : string { + return $this->placeRepository->chartDistribution($chart_shows, $chart_type, $surname); } /** - * A list of common countries. - * - * @return string + * @inheritDoc */ public function commonCountriesList(): string { - $countries = $this->statsPlaces(); - if (empty($countries)) { - return ''; - } - $top10 = []; - $i = 1; - // Get the country names for each language - $country_names = []; - 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 = []; - 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[] = '<li>'; - foreach ($country as $country_name => $tot) { - $tmp = new Place($country_name, $this->tree); - $place = '<a href="' . e($tmp->url()) . '" class="list_item">' . $all_countries[$country_code] . '</a>'; - $top10[] .= $place . ' - ' . I18N::number($tot); - } - $top10[] .= '</li>'; - if ($i++ == 10) { - break; - } - } - $top10 = implode('', $top10); - - return '<ul>' . $top10 . '</ul>'; + return $this->placeRepository->commonCountriesList(); } /** - * A list of common birth places. - * - * @return string + * @inheritDoc */ public function commonBirthPlacesList(): string { - $places = $this->statsPlaces('INDI', 'BIRT'); - $top10 = []; - $i = 1; - arsort($places); - foreach ($places as $place => $count) { - $tmp = new Place($place, $this->tree); - $place = '<a href="' . e($tmp->url()) . '" class="list_item">' . $tmp->fullName() . '</a>'; - $top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>'; - if ($i++ == 10) { - break; - } - } - $top10 = implode('', $top10); - - return '<ul>' . $top10 . '</ul>'; + return $this->placeRepository->commonBirthPlacesList(); } /** - * A list of common death places. - * - * @return string + * @inheritDoc */ public function commonDeathPlacesList(): string { - $places = $this->statsPlaces('INDI', 'DEAT'); - $top10 = []; - $i = 1; - arsort($places); - foreach ($places as $place => $count) { - $tmp = new Place($place, $this->tree); - $place = '<a href="' . e($tmp->url()) . '" class="list_item">' . $tmp->fullName() . '</a>'; - $top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>'; - if ($i++ == 10) { - break; - } - } - $top10 = implode('', $top10); - - return '<ul>' . $top10 . '</ul>'; + return $this->placeRepository->commonDeathPlacesList(); } /** - * A list of common marriage places. - * - * @return string + * @inheritDoc */ public function commonMarriagePlacesList(): string { - $places = $this->statsPlaces('FAM', 'MARR'); - $top10 = []; - $i = 1; - arsort($places); - foreach ($places as $place => $count) { - $tmp = new Place($place, $this->tree); - $place = '<a href="' . e($tmp->url()) . '" class="list_item">' . $tmp->fullName() . '</a>'; - $top10[] = '<li>' . $place . ' - ' . I18N::number($count) . '</li>'; - if ($i++ == 10) { - break; - } - } - $top10 = implode('', $top10); - - return '<ul>' . $top10 . '</ul>'; + return $this->placeRepository->commonMarriagePlacesList(); } /** - * Create a chart of birth places. - * - * @param bool $simple - * @param bool $sex - * @param int $year1 - * @param int $year2 - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return array|string - */ - public function statsBirthQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, string $size = null, string $color_from = null, string $color_to = null) - { - $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 FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " . - "WHERE " . - "d_file = {$this->tree->id()} AND " . - "d_year <> 0 AND " . - "d_fact='BIRT' AND " . - "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; - } elseif ($sex) { - $sql = - "SELECT 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->id()} AND " . - "d_fact='BIRT' AND " . - "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; - } else { - $sql = - "SELECT d_month, COUNT(*) AS total FROM `##dates` " . - "WHERE " . - "d_file={$this->tree->id()} 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) { - $size = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - - $sizes = explode('x', $size); - $tot = 0; - foreach ($rows as $values) { - $values->total = (int) $values->total; - $tot += $values->total; - } - // Beware divide by zero - if ($tot == 0) { - return ''; - } - $centuries = ''; - $counts = []; - foreach ($rows as $values) { - $counts[] = intdiv(100 * $values->total, $tot); - $centuries .= $this->centuryName($values->century) . ' - ' . I18N::number($values->total) . '|'; - } - $chd = $this->arrayToExtendedEncoding($counts); - $chl = rawurlencode(substr($centuries, 0, -1)); - - return "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Births by century') . '" title="' . I18N::translate('Births by century') . '" />'; - } - - return $rows; - } - - /** - * Create a chart of death places. - * - * @param bool $simple - * @param bool $sex - * @param int $year1 - * @param int $year2 - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return array|string - */ - public function statsDeathQuery($simple = true, $sex = false, $year1 = -1, $year2 = -1, string $size = null, string $color_from = null, string $color_to = null) - { - $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 FLOOR(d_year/100+1) AS century, COUNT(*) AS total FROM `##dates` " . - "WHERE " . - "d_file={$this->tree->id()} AND " . - 'd_year<>0 AND ' . - "d_fact='DEAT' AND " . - "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; - } elseif ($sex) { - $sql = - "SELECT 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->id()} AND " . - "d_fact='DEAT' AND " . - "d_type IN ('@#DGREGORIAN@', '@#DJULIAN@')"; - } else { - $sql = - "SELECT d_month, COUNT(*) AS total FROM `##dates` " . - "WHERE " . - "d_file={$this->tree->id()} 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) { - $size = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - - $sizes = explode('x', $size); - $tot = 0; - foreach ($rows as $values) { - $values->total = (int) $values->total; - $tot += $values->total; - } - // Beware divide by zero - if ($tot == 0) { - return ''; - } - $centuries = ''; - $counts = []; - foreach ($rows as $values) { - $counts[] = intdiv(100 * $values->total, $tot); - $centuries .= $this->centuryName($values->century) . ' - ' . I18N::number($values->total) . '|'; - } - $chd = $this->arrayToExtendedEncoding($counts); - $chl = rawurlencode(substr($centuries, 0, -1)); - - return "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Deaths by century') . '" title="' . I18N::translate('Deaths by century') . '" />'; - } - - return $rows; - } - - /** - * Find the earliest birth. - * - * @return string + * @inheritDoc */ public function firstBirth(): string { - return $this->mortalityQuery('full', 'ASC', 'BIRT'); + return $this->familyDatesRepository->firstBirth(); } /** - * Find the earliest birth year. - * - * @return string + * @inheritDoc */ public function firstBirthYear(): string { - return $this->mortalityQuery('year', 'ASC', 'BIRT'); + return $this->familyDatesRepository->firstBirthYear(); } /** - * Find the name of the earliest birth. - * - * @return string + * @inheritDoc */ public function firstBirthName(): string { - return $this->mortalityQuery('name', 'ASC', 'BIRT'); + return $this->familyDatesRepository->firstBirthName(); } /** - * Find the earliest birth place. - * - * @return string + * @inheritDoc */ public function firstBirthPlace(): string { - return $this->mortalityQuery('place', 'ASC', 'BIRT'); + return $this->familyDatesRepository->firstBirthPlace(); } /** - * Find the latest birth. - * - * @return string + * @inheritDoc */ public function lastBirth(): string { - return $this->mortalityQuery('full', 'DESC', 'BIRT'); + return $this->familyDatesRepository->lastBirth(); } /** - * Find the latest birth year. - * - * @return string + * @inheritDoc */ public function lastBirthYear(): string { - return $this->mortalityQuery('year', 'DESC', 'BIRT'); + return $this->familyDatesRepository->lastBirthYear(); } /** - * Find the latest birth name. - * - * @return string + * @inheritDoc */ public function lastBirthName(): string { - return $this->mortalityQuery('name', 'DESC', 'BIRT'); + return $this->familyDatesRepository->lastBirthName(); } /** - * Find the latest birth place. - * - * @return string + * @inheritDoc */ public function lastBirthPlace(): string { - return $this->mortalityQuery('place', 'DESC', 'BIRT'); + return $this->familyDatesRepository->lastBirthPlace(); } /** - * General query on births. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string + * @inheritDoc + */ + public function statsBirthQuery(bool $sex = false, int $year1 = -1, int $year2 = -1): array + { + return $this->individualRepository->statsBirthQuery($sex, $year1, $year2); + } + + /** + * @inheritDoc */ public function statsBirth(string $size = null, string $color_from = null, string $color_to = null): string { - return $this->statsBirthQuery(true, false, -1, -1, $size, $color_from, $color_to); + return $this->individualRepository->statsBirth($size, $color_from, $color_to); } /** - * Find the earliest death. - * - * @return string + * @inheritDoc */ public function firstDeath(): string { - return $this->mortalityQuery('full', 'ASC', 'DEAT'); + return $this->familyDatesRepository->firstDeath(); } /** - * Find the earliest death year. - * - * @return string + * @inheritDoc */ public function firstDeathYear(): string { - return $this->mortalityQuery('year', 'ASC', 'DEAT'); + return $this->familyDatesRepository->firstDeathYear(); } /** - * Find the earliest death name. - * - * @return string + * @inheritDoc */ public function firstDeathName(): string { - return $this->mortalityQuery('name', 'ASC', 'DEAT'); + return $this->familyDatesRepository->firstDeathName(); } /** - * Find the earliest death place. - * - * @return string + * @inheritDoc */ public function firstDeathPlace(): string { - return $this->mortalityQuery('place', 'ASC', 'DEAT'); + return $this->familyDatesRepository->firstDeathPlace(); } /** - * Find the latest death. - * - * @return string + * @inheritDoc */ public function lastDeath(): string { - return $this->mortalityQuery('full', 'DESC', 'DEAT'); + return $this->familyDatesRepository->lastDeath(); } /** - * Find the latest death year. - * - * @return string + * @inheritDoc */ public function lastDeathYear(): string { - return $this->mortalityQuery('year', 'DESC', 'DEAT'); + return $this->familyDatesRepository->lastDeathYear(); } /** - * Find the latest death name. - * - * @return string + * @inheritDoc */ public function lastDeathName(): string { - return $this->mortalityQuery('name', 'DESC', 'DEAT'); + return $this->familyDatesRepository->lastDeathName(); } /** - * Find the place of the latest death. - * - * @return string + * @inheritDoc */ public function lastDeathPlace(): string { - return $this->mortalityQuery('place', 'DESC', 'DEAT'); - } - - /** - * General query on deaths. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string - */ - public function statsDeath(string $size = null, string $color_from = null, string $color_to = null): string - { - return $this->statsDeathQuery(true, false, -1, -1, $size, $color_from, $color_to); - } - - /** - * Lifespan - * - * @param string $type - * @param string $sex - * - * @return string - */ - private function longlifeQuery($type, $sex): string - { - $sex_search = ' 1=1'; - if ($sex == 'F') { - $sex_search = " i_sex='F'"; - } elseif ($sex == 'M') { - $sex_search = " i_sex='M'"; - } - - $rows = $this->runSql( - " SELECT" . - " 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->id()} 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(); - } 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 = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>'; - break; - } - - return $result; + return $this->familyDatesRepository->lastDeathPlace(); } /** - * Find the oldest individuals. - * - * @param string $type - * @param string $sex - * @param int $total - * - * @return string + * @inheritDoc */ - private function topTenOldestQuery(string $type, string $sex, int $total): string + public function statsDeathQuery(bool $sex = false, int $year1 = -1, int $year2 = -1): array { - 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 " . - " 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->id()} 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 = []; - 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[] = '<li><a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')' . '</li>'; - } else { - $top10[] = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')'; - } - } - } - if ($type == 'list') { - $top10 = implode('', $top10); - } else { - $top10 = implode(' ', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type == 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; + return $this->individualRepository->statsDeathQuery($sex, $year1, $year2); } /** - * Find the oldest living individuals. - * - * @param string $type - * @param string $sex - * @param string $total - * - * @return string + * @inheritDoc */ - private function topTenOldestAliveQuery($type = 'list', $sex = 'BOTH', $total = '10'): string - { - $total = (int) $total; - - 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 = ''; - } - - $rows = $this->runSql( - "SELECT" . - " 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 (" . implode('|', Gedcom::DEATH_EVENTS) . ")' AND" . - " birth.d_file={$this->tree->id()} 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 = []; - 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[] = '<li><a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')' . '</li>'; - } else { - $top10[] = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a> (' . $age . ')'; - } - } - if ($type === 'list') { - $top10 = implode('', $top10); - } else { - $top10 = implode('; ', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type === 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; - } - - /** - * Find the average lifespan. - * - * @param string $sex - * @param bool $show_years - * - * @return string - */ - private function averageLifespanQuery($sex = 'BOTH', $show_years = false): string + public function statsDeath(string $size = null, string $color_from = null, string $color_to = null): string { - 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 IFNULL(AVG(death.d_julianday2-birth.d_julianday1), 0) 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->id() . " 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 - ); - - $age = $rows[0]->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); - } - - return I18N::number($age / 365.25); + return $this->individualRepository->statsDeath($size, $color_from, $color_to); } /** - * General query on ages. - * - * @param bool $simple - * @param string $related - * @param string $sex - * @param int $year1 - * @param int $year2 - * @param string $size - * - * @return array|string + * @inheritDoc */ - public function statsAgeQuery($simple = true, $related = 'BIRT', $sex = 'BOTH', $year1 = -1, $year2 = -1, $size = '230x250') + public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1) { - if ($simple) { - $sizes = explode('x', $size); - $rows = $this->runSql( - "SELECT" . - " 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->id()} 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 = []; - 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) . '|'; - - $female_age = $values['F'] ?? 0; - $male_age = $values['M'] ?? 0; - $average_age = $female_age + $male_age; - - if ($female_age > 0 && $male_age > 0) { - $average_age = $average_age / 2.0; - } - - $countsf .= (string) $female_age . ','; - $countsm .= (string) $male_age . ','; - $countsa .= (string) $average_age . ','; - } - - $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 = []; - while ($offset = strpos($title, ' ', $offset + 1)) { - $counter[] = $offset; - } - $half = intdiv(count($counter), 2); - $chtt = substr_replace($title, '|', $counter[$half], 1); - } - - return '<img src="' . "https://chart.googleapis.com/chart?cht=bvg&chs={$sizes[0]}x{$sizes[1]}&chm=D,FF0000,2,0,3,1|N*f1*,000000,0,-1,11,1|N*f1*,000000,1,-1,11,1&chf=bg,s,ffffff00|c,s,ffffff00&chtt=" . rawurlencode($chtt) . "&chd={$chd}&chco=0000FF,FFA0CB,FF0000&chbh=20,3&chxt=x,x,y,y&chxl=" . rawurlencode($chxl) . '&chdl=' . rawurlencode(I18N::translate('Males') . '|' . I18N::translate('Females') . '|' . I18N::translate('Average age at death')) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average age related to death century') . '" title="' . I18N::translate('Average age related to death century') . '" />'; - } - - $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" . - " 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->id()} 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; + return $this->individualRepository->statsAgeQuery($related, $sex, $year1, $year2); } /** - * General query on ages. - * - * @param string $size - * - * @return string + * @inheritDoc */ public function statsAge(string $size = '230x250'): string { - return $this->statsAgeQuery(true, 'BIRT', 'BOTH', -1, -1, $size); + return $this->individualRepository->statsAge($size); } /** - * Find the lognest lived individual. - * - * @return string + * @inheritDoc */ public function longestLife(): string { - return $this->longlifeQuery('full', 'BOTH'); + return $this->individualRepository->longestLife(); } /** - * Find the age of the longest lived individual. - * - * @return string + * @inheritDoc */ public function longestLifeAge(): string { - return $this->longlifeQuery('age', 'BOTH'); + return $this->individualRepository->longestLifeAge(); } /** - * Find the name of the longest lived individual. - * - * @return string + * @inheritDoc */ public function longestLifeName(): string { - return $this->longlifeQuery('name', 'BOTH'); + return $this->individualRepository->longestLifeName(); } /** - * Find the oldest individuals. - * - * @param string $total - * - * @return string + * @inheritDoc */ - public function topTenOldest(string $total = '10'): string + public function longestLifeFemale(): string { - return $this->topTenOldestQuery('nolist', 'BOTH', (int) $total); + return $this->individualRepository->longestLifeFemale(); } /** - * Find the oldest living individuals. - * - * @param string $total - * - * @return string + * @inheritDoc */ - public function topTenOldestList(string $total = '10'): string + public function longestLifeFemaleAge(): string { - return $this->topTenOldestQuery('list', 'BOTH', (int) $total); + return $this->individualRepository->longestLifeFemaleAge(); } /** - * Find the oldest living individuals. - * - * @param string|null $total - * - * @return string + * @inheritDoc */ - public function topTenOldestAlive(string $total = '10'): string + public function longestLifeFemaleName(): string { - return $this->topTenOldestAliveQuery('nolist', 'BOTH', $total); + return $this->individualRepository->longestLifeFemaleName(); } /** - * Find the oldest living individuals. - * - * @param string|null $total - * - * @return string + * @inheritDoc */ - public function topTenOldestListAlive(string $total = '10'): string + public function longestLifeMale(): string { - return $this->topTenOldestAliveQuery('list', 'BOTH', $total); + return $this->individualRepository->longestLifeMale(); } /** - * Find the average lifespan. - * - * @param bool $show_years - * - * @return string + * @inheritDoc */ - public function averageLifespan($show_years = false): string + public function longestLifeMaleAge(): string { - return $this->averageLifespanQuery('BOTH', $show_years); + return $this->individualRepository->longestLifeMaleAge(); } /** - * Find the longest lived female. - * - * @return string + * @inheritDoc */ - public function longestLifeFemale(): string + public function longestLifeMaleName(): string { - return $this->longlifeQuery('full', 'F'); + return $this->individualRepository->longestLifeMaleName(); } - /** - * Find the age of the longest lived female. - * - * @return string + * @inheritDoc */ - public function longestLifeFemaleAge(): string + public function topTenOldest(string $total = '10'): string { - return $this->longlifeQuery('age', 'F'); + return $this->individualRepository->topTenOldest((int) $total); } /** - * Find the name of the longest lived female. - * - * @return string + * @inheritDoc */ - public function longestLifeFemaleName(): string + public function topTenOldestList(string $total = '10'): string { - return $this->longlifeQuery('name', 'F'); + return $this->individualRepository->topTenOldestList((int) $total); } /** - * Find the oldest females. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topTenOldestFemale(string $total = '10'): string { - return $this->topTenOldestQuery('nolist', 'F', (int) $total); + return $this->individualRepository->topTenOldestFemale((int) $total); } /** - * Find the oldest living females. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topTenOldestFemaleList(string $total = '10'): string { - return $this->topTenOldestQuery('list', 'F', (int) $total); + return $this->individualRepository->topTenOldestFemaleList((int) $total); } /** - * Find the oldest living females. - * - * @param string|null $total - * - * @return string + * @inheritDoc */ - public function topTenOldestFemaleAlive(string $total = '10'): string + public function topTenOldestMale(string $total = '10'): string { - return $this->topTenOldestAliveQuery('nolist', 'F', $total); + return $this->individualRepository->topTenOldestMale((int) $total); } /** - * Find the oldest living females. - * - * @param string|null $total - * - * @return string + * @inheritDoc */ - public function topTenOldestFemaleListAlive(string $total = '10'): string + public function topTenOldestMaleList(string $total = '10'): string { - return $this->topTenOldestAliveQuery('list', 'F', $total); + return $this->individualRepository->topTenOldestMaleList((int) $total); } /** - * Find the average lifespan of females. - * - * @param bool $show_years - * - * @return string + * @inheritDoc */ - public function averageLifespanFemale($show_years = false): string + public function topTenOldestAlive(string $total = '10'): string { - return $this->averageLifespanQuery('F', $show_years); + return $this->individualRepository->topTenOldestAlive((int) $total); } /** - * Find the longest lived male. - * - * @return string + * @inheritDoc */ - public function longestLifeMale(): string + public function topTenOldestListAlive(string $total = '10'): string { - return $this->longlifeQuery('full', 'M'); + return $this->individualRepository->topTenOldestListAlive((int) $total); } /** - * Find the age of the longest lived male. - * - * @return string + * @inheritDoc */ - public function longestLifeMaleAge(): string + public function topTenOldestFemaleAlive(string $total = '10'): string { - return $this->longlifeQuery('age', 'M'); + return $this->individualRepository->topTenOldestFemaleAlive((int) $total); } /** - * Find the name of the longest lived male. - * - * @return string + * @inheritDoc */ - public function longestLifeMaleName(): string - { - return $this->longlifeQuery('name', 'M'); - } - - /** - * Find the longest lived males. - * - * @param string $total - * - * @return string - */ - public function topTenOldestMale(string $total = '10'): string + public function topTenOldestFemaleListAlive(string $total = '10'): string { - return $this->topTenOldestQuery('nolist', 'M', (int) $total); + return $this->individualRepository->topTenOldestFemaleListAlive((int) $total); } /** - * Find the longest lived males. - * - * @param string $total - * - * @return string + * @inheritDoc */ - public function topTenOldestMaleList(string $total = '10'): string + public function topTenOldestMaleAlive(string $total = '10'): string { - return $this->topTenOldestQuery('list', 'M', (int) $total); + return $this->individualRepository->topTenOldestMaleAlive((int) $total); } /** - * Find the longest lived living males. - * - * @param string|null $total - * - * @return string + * @inheritDoc */ - public function topTenOldestMaleAlive(string $total = '10'): string + public function topTenOldestMaleListAlive(string $total = '10'): string { - return $this->topTenOldestAliveQuery('nolist', 'M', $total); + return $this->individualRepository->topTenOldestMaleListAlive((int) $total); } /** - * Find the longest lived living males. - * - * @param string|null $total - * - * @return string + * @inheritDoc */ - public function topTenOldestMaleListAlive(string $total = '10'): string + public function averageLifespan(bool $show_years = false): string { - return $this->topTenOldestAliveQuery('list', 'M', $total); + return $this->individualRepository->averageLifespan($show_years); } /** - * Find the average male lifespan. - * - * @param bool $show_years - * - * @return string + * @inheritDoc */ - public function averageLifespanMale($show_years = false): string + public function averageLifespanFemale(bool $show_years = false): string { - return $this->averageLifespanQuery('M', $show_years); + return $this->individualRepository->averageLifespanFemale($show_years); } /** - * Events - * - * @param string $type - * @param string $direction - * @param string[] $facts - * - * @return string + * @inheritDoc */ - private function eventQuery(string $type, string $direction, array $facts): string + public function averageLifespanMale(bool $show_years = false): string { - $eventTypes = [ - '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 ('" . implode("','", $facts) . "')"; - - if ($direction != 'ASC') { - $direction = 'DESC'; - } - $rows = $this->runSql( - ' SELECT' . - ' d_gid AS id,' . - ' d_year AS year,' . - ' d_fact AS fact,' . - ' d_type AS type' . - ' FROM' . - " `##dates`" . - ' WHERE' . - " d_file={$this->tree->id()} 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(); - } 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 = '<a href="' . e($record->url()) . '">' . $record->getFullName() . '</a>'; - 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; + return $this->individualRepository->averageLifespanMale($show_years); } /** - * Find the earliest event. - * - * @return string + * @inheritDoc */ public function firstEvent(): string { - return $this->eventQuery('full', 'ASC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->firstEvent(); } /** - * Find the year of the earliest event. - * - * @return string + * @inheritDoc */ public function firstEventYear(): string { - return $this->eventQuery('year', 'ASC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->firstEventYear(); } /** - * Find the type of the earliest event. - * - * @return string + * @inheritDoc */ public function firstEventType(): string { - return $this->eventQuery('type', 'ASC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->firstEventType(); } /** - * Find the name of the individual with the earliest event. - * - * @return string + * @inheritDoc */ public function firstEventName(): string { - return $this->eventQuery('name', 'ASC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->firstEventName(); } /** - * Find the location of the earliest event. - * - * @return string + * @inheritDoc */ public function firstEventPlace(): string { - return $this->eventQuery('place', 'ASC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->firstEventPlace(); } /** - * Find the latest event. - * - * @return string + * @inheritDoc */ public function lastEvent(): string { - return $this->eventQuery('full', 'DESC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->lastEvent(); } /** - * Find the year of the latest event. - * - * @return string + * @inheritDoc */ public function lastEventYear(): string { - return $this->eventQuery('year', 'DESC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->lastEventYear(); } /** - * Find the type of the latest event. - * - * @return string + * @inheritDoc */ public function lastEventType(): string { - return $this->eventQuery('type', 'DESC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->lastEventType(); } /** - * Find the name of the individual with the latest event. - * - * @return string + * @inheritDoc */ public function lastEventName(): string { - return $this->eventQuery('name', 'DESC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); + return $this->eventRepository->lastEventName(); } /** - * FInd the location of the latest event. - * - * @return string + * @inheritDoc */ public function lastEventPlace(): string { - return $this->eventQuery('place', 'DESC', array_merge(Gedcom::BIRTH_EVENTS, Gedcom::MARRIAGE_EVENTS, Gedcom::DIVORCE_EVENTS, Gedcom::DEATH_EVENTS)); - } - - /** - * 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(string $type, string $age_dir, string $sex, bool $show_years): string - { - if ($sex == 'F') { - $sex_field = 'f_wife'; - } else { - $sex_field = 'f_husb'; - } - if ($age_dir != 'ASC') { - $age_dir = 'DESC'; - } - $rows = $this->runSql( - " SELECT 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->id()}" . - " LEFT JOIN `##dates` AS married ON married.d_file = {$this->tree->id()}" . - " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . - " 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->id()} 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(); - } else { - $result = I18N::translate('This information is private and cannot be shown.'); - } - break; - case 'name': - $result = '<a href="' . e($family->url()) . '">' . $person->getFullName() . '</a>'; - 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 int $total - * - * @return string - */ - private function ageOfMarriageQuery($type, $age_dir, int $total): string - { - $total = (int) $total; - - if ($age_dir != 'ASC') { - $age_dir = 'DESC'; - } - $hrows = $this->runSql( - " SELECT 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->id()}" . - " LEFT JOIN `##dates` AS husbdeath ON husbdeath.d_file = {$this->tree->id()}" . - " WHERE" . - " fam.f_file = {$this->tree->id()} 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 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->id()}" . - " LEFT JOIN `##dates` AS wifedeath ON wifedeath.d_file = {$this->tree->id()}" . - " WHERE" . - " fam.f_file = {$this->tree->id()} 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 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->id()}" . - " LEFT JOIN `##dates` AS divorced ON divorced.d_file = {$this->tree->id()}" . - " WHERE" . - " fam.f_file = {$this->tree->id()} 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}" - ); - $rows = []; - 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 = []; - $i = 0; - foreach ($rows as $fam => $age) { - $family = Family::getInstance($fam, $this->tree); - if ($type === 'name') { - return $family->formatList(); - } - 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[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')' . '</li>'; - } else { - $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')'; - } - } - if (++$i === $total) { - break; - } - } - } - if ($type === 'list') { - $top10 = implode('', $top10); - } else { - $top10 = implode('; ', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type === 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; - } - - /** - * Find the ages between spouses. - * - * @param string $type - * @param string $age_dir - * @param int $total - * - * @return string - */ - private function ageBetweenSpousesQuery(string $type, string $age_dir, int $total): string - { - $total = (int) $total; - - if ($age_dir === 'DESC') { - $sql = - "SELECT 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 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([ - 'tree_id' => $this->tree->id(), - 'limit' => $total, - ])->fetchAll(); - - $top10 = []; - 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[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')' . '</li>'; - } else { - $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> (' . $age . ')'; - } - } - } - if ($type === 'list') { - $top10 = implode('', $top10); - if ($top10) { - $top10 = '<ul>' . $top10 . '</ul>'; - } - } 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(string $type, string $age_dir, string $sex, bool $show_years): string - { - if ($sex == 'F') { - $sex_field = 'WIFE'; - } else { - $sex_field = 'HUSB'; - } - if ($age_dir != 'ASC') { - $age_dir = 'DESC'; - } - $rows = $this->runSql( - " SELECT" . - " 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->id()}" . - " JOIN `##dates` AS birth ON birth.d_file = {$this->tree->id()}" . - " JOIN `##dates` AS childbirth ON childbirth.d_file = {$this->tree->id()}" . - " 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->id()} 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(); - } else { - $result = I18N::translate('This information is private and cannot be shown.'); - } - break; - case 'name': - $result = '<a href="' . e($person->url()) . '">' . $person->getFullName() . '</a>'; - 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 = (string) floor($age / 365.25); - } - break; - } - - return $result; - } - - /** - * General query on marriages. - * - * @param bool $simple - * @param bool $first - * @param int $year1 - * @param int $year2 - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string|array - */ - public function statsMarrQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, string $size = null, string $color_from = null, string $color_to = null) - { - $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 FLOOR(d_year/100+1) AS century, COUNT(*) AS total" . - " FROM `##dates`" . - " WHERE d_file={$this->tree->id()} 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 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->id()}" . - " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . - " WHERE" . - " married.d_gid = fam.f_id AND" . - " fam.f_file = {$this->tree->id()} 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 d_month, COUNT(*) AS total" . - " FROM `##dates`" . - " WHERE d_file={$this->tree->id()} 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 ($simple) { - $size = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - - $sizes = explode('x', $size); - $tot = 0; - - foreach ($rows as $values) { - $values->total = (int) $values->total; - $tot += (int) $values->total; - } - // Beware divide by zero - if ($tot === 0) { - return ''; - } - $centuries = ''; - $counts = []; - foreach ($rows as $values) { - $counts[] = intdiv(100 * $values->total, $tot); - $centuries .= $this->centuryName($values->century) . ' - ' . I18N::number($values->total) . '|'; - } - $chd = $this->arrayToExtendedEncoding($counts); - $chl = substr($centuries, 0, -1); - - return "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Marriages by century') . '" title="' . I18N::translate('Marriages by century') . '" />'; - } - - return $rows; + return $this->eventRepository->lastEventType(); } /** - * General query on divorces. - * - * @param bool $simple - * @param bool $first - * @param int $year1 - * @param int $year2 - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string|stdClass[] - */ - private function statsDivQuery($simple = true, $first = false, $year1 = -1, $year2 = -1, string $size = null, string $color_from = null, string $color_to = null) - { - $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 FLOOR(d_year/100+1) AS century, COUNT(*) AS total" . - " FROM `##dates`" . - " WHERE d_file={$this->tree->id()} 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 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->id()}" . - " LEFT JOIN `##individuals` AS indi ON indi.i_file = {$this->tree->id()}" . - " WHERE" . - " divorced.d_gid = fam.f_id AND" . - " fam.f_file = {$this->tree->id()} 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 d_month, COUNT(*) AS total FROM `##dates` " . - "WHERE d_file={$this->tree->id()} 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 ($simple) { - $size = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - - $sizes = explode('x', $size); - $tot = 0; - foreach ($rows as $values) { - $values->total = (int) $values->total; - $tot += $values->total; - } - // Beware divide by zero - if ($tot === 0) { - return ''; - } - $centuries = ''; - $counts = []; - foreach ($rows as $values) { - $counts[] = intdiv(100 * $values->total, $tot); - $centuries .= $this->centuryName($values->century) . ' - ' . I18N::number($values->total) . '|'; - } - $chd = $this->arrayToExtendedEncoding($counts); - $chl = substr($centuries, 0, -1); - - return "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Divorces by century') . '" title="' . I18N::translate('Divorces by century') . '" />'; - } - - return $rows; - } - - /** - * Find the earliest marriage. - * - * @return string + * @inheritDoc */ public function firstMarriage(): string { - return $this->mortalityQuery('full', 'ASC', 'MARR'); + return $this->familyDatesRepository->firstMarriage(); } /** - * Find the year of the earliest marriage. - * - * @return string + * @inheritDoc */ public function firstMarriageYear(): string { - return $this->mortalityQuery('year', 'ASC', 'MARR'); + return $this->familyDatesRepository->firstMarriageYear(); } /** - * Find the names of spouses of the earliest marriage. - * - * @return string + * @inheritDoc */ public function firstMarriageName(): string { - return $this->mortalityQuery('name', 'ASC', 'MARR'); + return $this->familyDatesRepository->firstMarriageName(); } /** - * Find the place of the earliest marriage. - * - * @return string + * @inheritDoc */ public function firstMarriagePlace(): string { - return $this->mortalityQuery('place', 'ASC', 'MARR'); + return $this->familyDatesRepository->firstMarriagePlace(); } /** - * Find the latest marriage. - * - * @return string + * @inheritDoc */ public function lastMarriage(): string { - return $this->mortalityQuery('full', 'DESC', 'MARR'); + return $this->familyDatesRepository->lastMarriage(); } /** - * Find the year of the latest marriage. - * - * @return string + * @inheritDoc */ public function lastMarriageYear(): string { - return $this->mortalityQuery('year', 'DESC', 'MARR'); + return $this->familyDatesRepository->lastMarriageYear(); } /** - * Find the names of spouses of the latest marriage. - * - * @return string + * @inheritDoc */ public function lastMarriageName(): string { - return $this->mortalityQuery('name', 'DESC', 'MARR'); + return $this->familyDatesRepository->lastMarriageName(); } /** - * Find the location of the latest marriage. - * - * @return string + * @inheritDoc */ public function lastMarriagePlace(): string { - return $this->mortalityQuery('place', 'DESC', 'MARR'); + return $this->familyDatesRepository->lastMarriagePlace(); } /** - * General query on marriages. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string + * @inheritDoc + */ + public function statsMarrQuery(bool $first = false, int $year1 = -1, int $year2 = -1): array + { + return $this->familyRepository->statsMarrQuery($first, $year1, $year2); + } + + /** + * @inheritDoc */ public function statsMarr(string $size = null, string $color_from = null, string $color_to = null): string { - return $this->statsMarrQuery(true, false, -1, -1, $size, $color_from, $color_to); + return $this->familyRepository->statsMarr($size, $color_from, $color_to); } /** - * Find the earliest divorce. - * - * @return string + * @inheritDoc */ public function firstDivorce(): string { - return $this->mortalityQuery('full', 'ASC', 'DIV'); + return $this->familyDatesRepository->firstDivorce(); } /** - * Find the year of the earliest divorce. - * - * @return string + * @inheritDoc */ public function firstDivorceYear(): string { - return $this->mortalityQuery('year', 'ASC', 'DIV'); + return $this->familyDatesRepository->firstDivorceYear(); } /** - * Find the names of individuals in the earliest divorce. - * - * @return string + * @inheritDoc */ public function firstDivorceName(): string { - return $this->mortalityQuery('name', 'ASC', 'DIV'); + return $this->familyDatesRepository->firstDivorceName(); } /** - * Find the location of the earliest divorce. - * - * @return string + * @inheritDoc */ public function firstDivorcePlace(): string { - return $this->mortalityQuery('place', 'ASC', 'DIV'); + return $this->familyDatesRepository->firstDivorcePlace(); } /** - * Find the latest divorce. - * - * @return string + * @inheritDoc */ public function lastDivorce(): string { - return $this->mortalityQuery('full', 'DESC', 'DIV'); + return $this->familyDatesRepository->lastDivorce(); } /** - * Find the year of the latest divorce. - * - * @return string + * @inheritDoc */ public function lastDivorceYear(): string { - return $this->mortalityQuery('year', 'DESC', 'DIV'); + return $this->familyDatesRepository->lastDivorceYear(); } /** - * Find the names of the individuals in the latest divorce. - * - * @return string + * @inheritDoc */ public function lastDivorceName(): string { - return $this->mortalityQuery('name', 'DESC', 'DIV'); + return $this->familyDatesRepository->lastDivorceName(); } /** - * Find the location of the latest divorce. - * - * @return string + * @inheritDoc */ public function lastDivorcePlace(): string { - return $this->mortalityQuery('place', 'DESC', 'DIV'); + return $this->familyDatesRepository->lastDivorcePlace(); } /** - * General divorce query. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * - * @return string + * @inheritDoc */ public function statsDiv(string $size = null, string $color_from = null, string $color_to = null): string { - return $this->statsDivQuery(true, false, -1, -1, $size, $color_from, $color_to); - } - - /** - * General query on ages at marriage. - * - * @param bool $simple - * @param string $sex - * @param int $year1 - * @param int $year2 - * @param string $size - * - * @return array|string - */ - public function statsMarrAgeQuery($simple = true, $sex = 'M', $year1 = -1, $year2 = -1, $size = '200x250') - { - if ($simple) { - $sizes = explode('x', $size); - - $rows = $this->runSql( - "SELECT " . - " 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->id()} 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->id()} 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) { - $values->age = (int) $values->age; - if ($max < $values->age) { - $max = $values->age; - } - } - $chxl = '0:|'; - $chmm = ''; - $chmf = ''; - $i = 0; - $countsm = ''; - $countsf = ''; - $countsa = ''; - $out = []; - 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 = []; - while ($offset = strpos(I18N::translate('Average age in century of marriage'), ' ', $offset + 1)) { - $counter[] = $offset; - } - $half = intdiv(count($counter), 2); - $chtt = substr_replace(I18N::translate('Average age in century of marriage'), '|', $counter[$half], 1); - } - - return '<img src="' . "https://chart.googleapis.com/chart?cht=bvg&chs={$sizes[0]}x{$sizes[1]}&chm=D,FF0000,2,0,3,1|{$chmm}{$chmf}&chf=bg,s,ffffff00|c,s,ffffff00&chtt=" . rawurlencode($chtt) . "&chd={$chd}&chco=0000FF,FFA0CB,FF0000&chbh=20,3&chxt=x,x,y,y&chxl=" . rawurlencode($chxl) . '&chdl=' . rawurlencode(I18N::translate('Males') . '|' . I18N::translate('Females') . '|' . I18N::translate('Average age')) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average age in century of marriage') . '" title="' . I18N::translate('Average age in century of marriage') . '" />'; - } - - if ($year1 >= 0 && $year2 >= 0) { - $years = " married.d_year BETWEEN {$year1} AND {$year2} AND "; - } else { - $years = ''; - } - $rows = $this->runSql( - "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_husb AND birth.d_file=fam.f_file) " . - "WHERE " . - " '{$sex}' IN ('M', 'BOTH') AND {$years} " . - " married.d_file={$this->tree->id()} 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->id()} 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; + return $this->familyRepository->statsDiv($size, $color_from, $color_to); } /** - * Find the youngest wife. - * - * @return string + * @inheritDoc */ public function youngestMarriageFemale(): string { - return $this->marriageQuery('full', 'ASC', 'F', false); + return $this->familyRepository->youngestMarriageFemale(); } /** - * Find the name of the youngest wife. - * - * @return string + * @inheritDoc */ public function youngestMarriageFemaleName(): string { - return $this->marriageQuery('name', 'ASC', 'F', false); + return $this->familyRepository->youngestMarriageFemaleName(); } /** - * Find the age of the youngest wife. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function youngestMarriageFemaleAge(string $show_years = ''): string { - return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years); + return $this->familyRepository->youngestMarriageFemaleAge($show_years); } /** - * Find the oldest wife. - * - * @return string + * @inheritDoc */ public function oldestMarriageFemale(): string { - return $this->marriageQuery('full', 'DESC', 'F', false); + return $this->familyRepository->oldestMarriageFemale(); } /** - * Find the name of the oldest wife. - * - * @return string + * @inheritDoc */ public function oldestMarriageFemaleName(): string { - return $this->marriageQuery('name', 'DESC', 'F', false); + return $this->familyRepository->oldestMarriageFemaleName(); } /** - * Find the age of the oldest wife. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function oldestMarriageFemaleAge(string $show_years = ''): string { - return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years); + return $this->familyRepository->oldestMarriageFemaleAge($show_years); } /** - * Find the youngest husband. - * - * @return string + * @inheritDoc */ public function youngestMarriageMale(): string { - return $this->marriageQuery('full', 'ASC', 'M', false); + return $this->familyRepository->youngestMarriageMale(); } /** - * Find the name of the youngest husband. - * - * @return string + * @inheritDoc */ public function youngestMarriageMaleName(): string { - return $this->marriageQuery('name', 'ASC', 'M', false); + return $this->familyRepository->youngestMarriageMaleName(); } /** - * Find the age of the youngest husband. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function youngestMarriageMaleAge(string $show_years = ''): string { - return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years); + return $this->familyRepository->youngestMarriageMaleAge($show_years); } /** - * Find the oldest husband. - * - * @return string + * @inheritDoc */ public function oldestMarriageMale(): string { - return $this->marriageQuery('full', 'DESC', 'M', false); + return $this->familyRepository->oldestMarriageMale(); } /** - * Find the name of the oldest husband. - * - * @return string + * @inheritDoc */ public function oldestMarriageMaleName(): string { - return $this->marriageQuery('name', 'DESC', 'M', false); + return $this->familyRepository->oldestMarriageMaleName(); } /** - * Find the age of the oldest husband. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function oldestMarriageMaleAge(string $show_years = ''): string { - return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years); + return $this->familyRepository->oldestMarriageMaleAge($show_years); } /** - * General query on marriage ages. - * - * @param string $size - * - * @return string + * @inheritDoc + */ + public function statsMarrAgeQuery(string $sex = 'M', int $year1 = -1, int $year2 = -1): array + { + return $this->familyRepository->statsMarrAgeQuery($sex, $year1, $year2); + } + + /** + * @inheritDoc */ public function statsMarrAge(string $size = '200x250'): string { - return $this->statsMarrAgeQuery(true, 'BOTH', -1, -1, $size); + return $this->familyRepository->statsMarrAge($size); } /** - * Find the age between husband and wife. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function ageBetweenSpousesMF(string $total = '10'): string { - return $this->ageBetweenSpousesQuery('nolist', 'DESC', (int) $total); + return $this->familyRepository->ageBetweenSpousesMF((int) $total); } /** - * Find the age between husband and wife. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function ageBetweenSpousesMFList(string $total = '10'): string { - return $this->ageBetweenSpousesQuery('list', 'DESC', (int) $total); + return $this->familyRepository->ageBetweenSpousesMFList((int) $total); } /** - * Find the age between wife and husband.. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function ageBetweenSpousesFM(string $total = '10'): string { - return $this->ageBetweenSpousesQuery('nolist', 'ASC', (int) $total); + return $this->familyRepository->ageBetweenSpousesFM((int) $total); } /** - * Find the age between wife and husband.. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function ageBetweenSpousesFMList(string $total = '10'): string { - return $this->ageBetweenSpousesQuery('list', 'ASC', (int) $total); + return $this->familyRepository->ageBetweenSpousesFMList((int) $total); } /** - * General query on marriage ages. - * - * @return string + * @inheritDoc */ public function topAgeOfMarriageFamily(): string { - return $this->ageOfMarriageQuery('name', 'DESC', 1); + return $this->familyRepository->topAgeOfMarriageFamily(); } /** - * General query on marriage ages. - * - * @return string + * @inheritDoc */ public function topAgeOfMarriage(): string { - return $this->ageOfMarriageQuery('age', 'DESC', 1); + return $this->familyRepository->topAgeOfMarriage(); } /** - * General query on marriage ages. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topAgeOfMarriageFamilies(string $total = '10'): string { - return $this->ageOfMarriageQuery('nolist', 'DESC', (int) $total); + return $this->familyRepository->topAgeOfMarriageFamilies((int) $total); } /** - * General query on marriage ages. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topAgeOfMarriageFamiliesList(string $total = '10'): string { - return $this->ageOfMarriageQuery('list', 'DESC', (int) $total); + return $this->familyRepository->topAgeOfMarriageFamiliesList((int) $total); } /** - * General query on marriage ages. - * - * @return string + * @inheritDoc */ public function minAgeOfMarriageFamily(): string { - return $this->ageOfMarriageQuery('name', 'ASC', 1); + return $this->familyRepository->minAgeOfMarriageFamily(); } /** - * General query on marriage ages. - * - * @return string + * @inheritDoc */ public function minAgeOfMarriage(): string { - return $this->ageOfMarriageQuery('age', 'ASC', 1); + return $this->familyRepository->minAgeOfMarriage(); } /** - * General query on marriage ages. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function minAgeOfMarriageFamilies(string $total = '10'): string { - return $this->ageOfMarriageQuery('nolist', 'ASC', (int) $total); + return $this->familyRepository->minAgeOfMarriageFamilies((int) $total); } /** - * General query on marriage ages. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function minAgeOfMarriageFamiliesList(string $total = '10'): string { - return $this->ageOfMarriageQuery('list', 'ASC', (int) $total); + return $this->familyRepository->minAgeOfMarriageFamiliesList((int) $total); } /** - * Find the youngest mother - * - * @return string + * @inheritDoc */ public function youngestMother(): string { - return $this->parentsQuery('full', 'ASC', 'F', false); + return $this->familyRepository->youngestMother(); } /** - * Find the name of the youngest mother. - * - * @return string + * @inheritDoc */ public function youngestMotherName(): string { - return $this->parentsQuery('name', 'ASC', 'F', false); + return $this->familyRepository->youngestMotherName(); } /** - * Find the age of the youngest mother. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function youngestMotherAge(string $show_years = ''): string { - return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years); + return $this->familyRepository->youngestMotherAge($show_years); } /** - * Find the oldest mother. - * - * @return string + * @inheritDoc */ public function oldestMother(): string { - return $this->parentsQuery('full', 'DESC', 'F', false); + return $this->familyRepository->oldestMother(); } /** - * Find the name of the oldest mother. - * - * @return string + * @inheritDoc */ public function oldestMotherName(): string { - return $this->parentsQuery('name', 'DESC', 'F', false); + return $this->familyRepository->oldestMotherName(); } /** - * Find the age of the oldest mother. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function oldestMotherAge(string $show_years = ''): string { - return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years); + return $this->familyRepository->oldestMotherAge($show_years); } /** - * Find the youngest father. - * - * @return string + * @inheritDoc */ public function youngestFather(): string { - return $this->parentsQuery('full', 'ASC', 'M', false); + return $this->familyRepository->youngestFather(); } /** - * Find the name of the youngest father. - * - * @return string + * @inheritDoc */ public function youngestFatherName(): string { - return $this->parentsQuery('name', 'ASC', 'M', false); + return $this->familyRepository->youngestFatherName(); } /** - * Find the age of the youngest father. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function youngestFatherAge(string $show_years = ''): string { - return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years); + return $this->familyRepository->youngestFatherAge($show_years); } /** - * Find the oldest father. - * - * @return string + * @inheritDoc */ public function oldestFather(): string { - return $this->parentsQuery('full', 'DESC', 'M', false); + return $this->familyRepository->oldestFather(); } /** - * Find the name of the oldest father. - * - * @return string + * @inheritDoc */ public function oldestFatherName(): string { - return $this->parentsQuery('name', 'DESC', 'M', false); + return $this->familyRepository->oldestFatherName(); } /** - * Find the age of the oldest father. - * - * @param string $show_years - * - * @return string + * @inheritDoc */ public function oldestFatherAge(string $show_years = ''): string { - return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years); + return $this->familyRepository->oldestFatherAge($show_years); } /** - * Number of husbands. - * - * @return string + * @inheritDoc */ public function totalMarriedMales(): string { - $n = (int) Database::prepare( - "SELECT COUNT(DISTINCT f_husb) FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\\n1 MARR%'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - - return I18N::number($n); + return $this->familyRepository->totalMarriedMales(); } /** - * Number of wives. - * - * @return string + * @inheritDoc */ public function totalMarriedFemales(): string { - $n = (int) Database::prepare( - "SELECT COUNT(DISTINCT f_wife) FROM `##families` WHERE f_file = :tree_id AND f_gedcom LIKE '%\\n1 MARR%'" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - - return I18N::number($n); + return $this->familyRepository->totalMarriedFemales(); } /** - * General query on family. - * - * @param string $type - * - * @return string + * @inheritDoc */ - private function familyQuery($type): string + public function monthFirstChildQuery(bool $sex = false): array { - $rows = $this->runSql( - " SELECT f_numchil AS tot, f_id AS id" . - " FROM `##families`" . - " WHERE" . - " f_file={$this->tree->id()}" . - " AND f_numchil = (" . - " SELECT max( f_numchil )" . - " FROM `##families`" . - " WHERE f_file ={$this->tree->id()}" . - " )" . - " 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(); - } else { - $result = I18N::translate('This information is private and cannot be shown.'); - } - break; - case 'size': - $result = I18N::number((int) $row->tot); - break; - case 'name': - $result = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a>'; - break; - } - - return $result; + return $this->familyRepository->monthFirstChildQuery($sex); } /** - * General query on families. - * - * @param string $type - * @param int $total - * - * @return string - */ - private function topTenFamilyQuery(string $type, int $total): string - { - $total = (int) $total; - - $rows = $this->runSql( - "SELECT f_numchil AS tot, f_id AS id" . - " FROM `##families`" . - " WHERE" . - " f_file={$this->tree->id()}" . - " ORDER BY tot DESC" . - " LIMIT " . $total - ); - - if (empty($rows)) { - return ''; - } - - $top10 = []; - foreach ($rows as $row) { - $family = Family::getInstance($row->id, $this->tree); - if ($family->canShow()) { - $total = (int) $row->tot; - - if ($type === 'list') { - $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s child', '%s children', $total, I18N::number($total)); - } else { - $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s child', '%s children', $total, I18N::number($total)); - } - } - } - if ($type === 'list') { - $top10 = implode('', $top10); - } else { - $top10 = implode('; ', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type === 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; - } - - /** - * Find the ages between siblings. - * - * @param string $type - * @param int $total - * @param bool $one Include each family only once if true - * - * @return string - */ - private function ageBetweenSiblingsQuery(string $type, int $total, bool $one): string - { - $rows = $this->runSql( - " SELECT 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->id()}" . - " LEFT JOIN `##dates` AS child2 ON child2.d_file = {$this->tree->id()}" . - " LEFT JOIN `##link` AS link2 ON link2.l_file = {$this->tree->id()}" . - " WHERE" . - " link1.l_file = {$this->tree->id()} 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 = []; - $dist = []; - 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 = '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> '; - $return .= I18N::translate('and') . ' '; - $return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>'; - $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; - } 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 = '<li>'; - $return .= '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> '; - $return .= I18N::translate('and') . ' '; - $return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>'; - $return .= ' (' . $age . ')'; - $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; - $return .= '</li>'; - $top10[] = $return; - $dist[] = $fam->family; - } - } elseif (!$one && $child1->canShow() && $child2->canShow()) { - $return = '<li>'; - $return .= '<a href="' . e($child2->url()) . '">' . $child2->getFullName() . '</a> '; - $return .= I18N::translate('and') . ' '; - $return .= '<a href="' . e($child1->url()) . '">' . $child1->getFullName() . '</a>'; - $return .= ' (' . $age . ')'; - $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; - $return .= '</li>'; - $top10[] = $return; - } - } else { - if ($child1->canShow() && $child2->canShow()) { - $return = $child2->formatList(); - $return .= '<br>' . I18N::translate('and') . '<br>'; - $return .= $child1->formatList(); - $return .= '<br><a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; - - return $return; - } - - return I18N::translate('This information is private and cannot be shown.'); - } - } - if ($type === 'list') { - $top10 = implode('', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type === 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; - } - - /** - * Find the month in the year of the birth of the first child. - * - * @param bool $sex - * @param int $year1 - * @param int $year2 - * - * @return stdClass[] - */ - public function monthFirstChildQuery($sex = false, $year1 = -1, $year2 = -1): array - { - 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 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->id()}" . - $sql_sex2 . - " WHERE" . - " link1.l_file = {$this->tree->id()} AND" . - " link1.l_type = 'CHIL' AND" . - " child1.d_gid = link1.l_to AND" . - " child1.d_fact = 'BIRT' AND" . - " child1.d_month IN ('JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC')" . - $sql_years . - " ORDER BY date" . - " ) AS children" . - " GROUP BY family, d_month{$sql_sex1}" . - ") AS first_child " . - "GROUP BY d_month"; - if ($sex) { - $sql .= ', i_sex'; - } - $rows = $this->runSql($sql); - - return $rows; - } - - /** - * Find the family with the most children. - * - * @return string + * @inheritDoc */ public function largestFamily(): string { - return $this->familyQuery('full'); + return $this->familyRepository->largestFamily(); } /** - * Find the number of children in the largest family. - * - * @return string + * @inheritDoc */ public function largestFamilySize(): string { - return $this->familyQuery('size'); + return $this->familyRepository->largestFamilySize(); } /** - * Find the family with the most children. - * - * @return string + * @inheritDoc */ public function largestFamilyName(): string { - return $this->familyQuery('name'); + return $this->familyRepository->largestFamilyName(); } /** - * The the families with the most children. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topTenLargestFamily(string $total = '10'): string { - return $this->topTenFamilyQuery('nolist', (int) $total); + return $this->familyRepository->topTenLargestFamily((int) $total); } /** - * Find the families with the most children. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topTenLargestFamilyList(string $total = '10'): string { - return $this->topTenFamilyQuery('list', (int) $total); + return $this->familyRepository->topTenLargestFamilyList((int) $total); } /** - * Create a chart of the largest families. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * @param string $total - * - * @return string + * @inheritDoc */ - public function chartLargestFamilies(string $size = null, string $color_from = null, string $color_to = null, string $total = '10'): string - { - $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'); - - $size = $size ?? $WT_STATS_L_CHART_X . 'x' . $WT_STATS_S_CHART_Y; - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - $total = $total ?? '10'; - - $sizes = explode('x', $size); - $total = (int) $total; - $rows = $this->runSql( - " SELECT f_numchil AS tot, f_id AS id" . - " FROM `##families`" . - " WHERE f_file={$this->tree->id()}" . - " ORDER BY tot DESC" . - " LIMIT " . $total - ); - if (!isset($rows[0])) { - return ''; - } - $tot = 0; - foreach ($rows as $row) { - $row->tot = (int) $row->tot; - $tot += $row->tot; - } - $chd = ''; - $chl = []; - foreach ($rows as $row) { - $family = Family::getInstance($row->id, $this->tree); - if ($family->canShow()) { - if ($tot == 0) { - $per = 0; - } else { - $per = intdiv(100 * $row->tot, $tot); - } - $chd .= $this->arrayToExtendedEncoding([$per]); - $chl[] = htmlspecialchars_decode(strip_tags($family->getFullName())) . ' - ' . I18N::number($row->tot); - } - } - $chl = rawurlencode(implode('|', $chl)); - - return "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl={$chl}\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Largest families') . '" title="' . I18N::translate('Largest families') . '" />'; + public function chartLargestFamilies( + string $size = null, + string $color_from = null, + string $color_to = null, + string $total = '10' + ): string { + return $this->familyRepository->chartLargestFamilies($size, $color_from, $color_to, (int) $total); } /** - * Count the total children. - * - * @return string + * @inheritDoc */ public function totalChildren(): string { - $total = (int) Database::prepare( - "SELECT SUM(f_numchil) FROM `##families` WHERE f_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - - return I18N::number($total); + return $this->familyRepository->totalChildren(); } /** - * Find the average number of children in families. - * - * @return string + * @inheritDoc */ public function averageChildren(): string { - $average = (float) Database::prepare( - "SELECT AVG(f_numchil) AS tot FROM `##families` WHERE f_file = :tree_id" - )->execute([ - 'tree_id' => $this->tree->id(), - ])->fetchOne(); - - return I18N::number($average, 2); + return $this->familyRepository->averageChildren(); } /** - * General query on familes/children. - * - * @param bool $simple - * @param string $sex - * @param int $year1 - * @param int $year2 - * @param string $size - * - * @return string|stdClass[] + * @inheritDoc */ - public function statsChildrenQuery($simple = true, $sex = 'BOTH', $year1 = -1, $year2 = -1, $size = '220x200') + public function statsChildrenQuery(string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array { - if ($simple) { - $sizes = explode('x', $size); - $max = 0; - $rows = $this->runSql( - " SELECT 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->id()}" . - " 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) { - $values->num = (int) $values->num; - if ($max < $values->num) { - $max = $values->num; - } - } - $chm = ''; - $chxl = '0:|'; - $i = 0; - $counts = []; - foreach ($rows as $values) { - $chxl .= $this->centuryName($values->century) . '|'; - if ($max <= 5) { - $counts[] = (int) ($values->num * 819.2 - 1); - } elseif ($max <= 10) { - $counts[] = (int) ($values->num * 409.6); - } else { - $counts[] = (int) ($values->num * 204.8); - } - $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 "<img src=\"https://chart.googleapis.com/chart?cht=bvg&chs={$sizes[0]}x{$sizes[1]}&chf=bg,s,ffffff00|c,s,ffffff00&chm=D,FF0000,0,0,3,1|{$chm}&chd=e:{$chd}&chco=0000FF&chbh=30,3&chxt=x,x,y,y&chxl=" . rawurlencode($chxl) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Average number of children per family') . '" title="' . I18N::translate('Average number of children per family') . '" />'; - } - - if ($sex == 'M') { - $sql = - "SELECT 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->id()} GROUP BY l_to" . - ") boys" . - " GROUP BY num" . - " ORDER BY num"; - } elseif ($sex == 'F') { - $sql = - "SELECT 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->id()} GROUP BY l_to" . - ") girls" . - " GROUP BY num" . - " ORDER BY num"; - } else { - $sql = "SELECT 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->id()}" - . " WHERE" - . " married.d_gid = fam.f_id AND" - . " fam.f_file = {$this->tree->id()} AND" - . " married.d_fact = 'MARR' AND" - . " married.d_year BETWEEN '{$year1}' AND '{$year2}'"; - } else { - $sql .= "WHERE f_file={$this->tree->id()}"; - } - $sql .= ' GROUP BY f_numchil'; - } - $rows = $this->runSql($sql); - - return $rows; + return $this->familyRepository->statsChildrenQuery($sex, $year1, $year2); } /** - * Genearl query on families/children. - * - * @param string $size - * - * @return string + * @inheritDoc */ public function statsChildren(string $size = '220x200'): string { - return $this->statsChildrenQuery(true, 'BOTH', -1, -1, $size); + return $this->familyRepository->statsChildren($size); } /** - * Find the names of siblings with the widest age gap. - * - * @param string $total - * @param string $one - * - * @return string + * @inheritDoc */ - public function topAgeBetweenSiblingsName(string $total = '10', string $one = ''): string + public function topAgeBetweenSiblingsName(string $total = '10'): string { - return $this->ageBetweenSiblingsQuery('name', (int) $total, (bool) $one); + return $this->familyRepository->topAgeBetweenSiblingsName((int) $total); } /** - * Find the widest age gap between siblings. - * - * @param string $total - * @param string $one - * - * @return string + * @inheritDoc */ - public function topAgeBetweenSiblings(string $total = '10', string $one = ''): string + public function topAgeBetweenSiblings(string $total = '10'): string { - return $this->ageBetweenSiblingsQuery('age', (int) $total, (bool) $one); + return $this->familyRepository->topAgeBetweenSiblings((int) $total); } /** - * Find the name of siblings with the widest age gap. - * - * @param string $total - * @param string $one - * - * @return string + * @inheritDoc */ - public function topAgeBetweenSiblingsFullName(string $total = '10', string $one = ''): string + public function topAgeBetweenSiblingsFullName(string $total = '10'): string { - return $this->ageBetweenSiblingsQuery('nolist', (int) $total, (bool) $one); + return $this->familyRepository->topAgeBetweenSiblingsFullName((int) $total); } /** - * Find the siblings with the widest age gaps. - * - * @param string $total - * @param string $one - * - * @return string + * @inheritDoc */ public function topAgeBetweenSiblingsList(string $total = '10', string $one = ''): string { - return $this->ageBetweenSiblingsQuery('list', (int) $total, (bool) $one); + return $this->familyRepository->topAgeBetweenSiblingsList((int) $total, $one); } /** - * Find the families with no children. - * - * @return int - */ - private function noChildrenFamiliesQuery(): int - { - $rows = $this->runSql( - " SELECT COUNT(*) AS tot" . - " FROM `##families`" . - " WHERE f_numchil = 0 AND f_file = {$this->tree->id()}" - ); - - return (int) $rows[0]->tot; - } - - /** - * Find the families with no children. - * - * @return string + * @inheritDoc */ public function noChildrenFamilies(): string { - return I18N::number($this->noChildrenFamiliesQuery()); - } - - /** - * Find the families with no children. - * - * @param string $type - * - * @return string - */ - public function noChildrenFamiliesList($type = 'list'): string - { - $rows = $this->runSql( - " SELECT f_id AS family" . - " FROM `##families` AS fam" . - " WHERE f_numchil = 0 AND fam.f_file = {$this->tree->id()}" - ); - if (!isset($rows[0])) { - return ''; - } - $top10 = []; - foreach ($rows as $row) { - $family = Family::getInstance($row->family, $this->tree); - if ($family->canShow()) { - if ($type == 'list') { - $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a></li>'; - } else { - $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a>'; - } - } - } - if ($type == 'list') { - $top10 = implode('', $top10); - } else { - $top10 = implode('; ', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type === 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; + return $this->familyRepository->noChildrenFamilies(); } /** - * Create a chart of children with no families. - * - * @param string $size - * @param string $year1 - * @param string $year2 - * - * @return string + * @inheritDoc */ - public function chartNoChildrenFamilies(string $size = '220x200', $year1 = '-1', $year2 = '-1'): string + public function noChildrenFamiliesList(string $type = 'list'): string { - $year1 = (int) $year1; - $year2 = (int) $year2; - - $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" . - " 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->id()} 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) { - $values->count = (int) $values->count; - - if ($max < $values->count) { - $max = $values->count; - } - $tot += $values->count; - } - $unknown = $this->noChildrenFamiliesQuery() - $tot; - if ($unknown > $max) { - $max = $unknown; - } - $chm = ''; - $chxl = '0:|'; - $i = 0; - $counts = []; - foreach ($rows as $values) { - $chxl .= $this->centuryName($values->century) . '|'; - $counts[] = intdiv(4095 * $values->count, $max + 1); - $chm .= 't' . $values->count . ',000000,0,' . $i . ',11,1|'; - $i++; - } - $counts[] = intdiv(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 "<img src=\"https://chart.googleapis.com/chart?cht=bvg&chs={$sizes[0]}x{$sizes[1]}&chf=bg,s,ffffff00|c,s,ffffff00&chm=D,FF0000,0,0:" . ($i - 1) . ",3,1|{$chm}&chd=e:{$chd}&chco=0000FF,ffffff00&chbh=30,3&chxt=x,x,y,y&chxl=" . rawurlencode($chxl) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . I18N::translate('Number of families without children') . '" title="' . I18N::translate('Number of families without children') . '" />'; + return $this->familyRepository->noChildrenFamiliesList($type); } /** - * Find the couple with the most grandchildren. - * - * @param string $type - * @param int $total - * - * @return string + * @inheritDoc */ - private function topTenGrandFamilyQuery(string $type, int $total): string - { - $rows = $this->runSql( - "SELECT COUNT(*) AS tot, f_id AS id" . - " FROM `##families`" . - " JOIN `##link` AS children ON children.l_file = {$this->tree->id()}" . - " JOIN `##link` AS mchildren ON mchildren.l_file = {$this->tree->id()}" . - " JOIN `##link` AS gchildren ON gchildren.l_file = {$this->tree->id()}" . - " WHERE" . - " f_file={$this->tree->id()} 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 = []; - foreach ($rows as $row) { - $family = Family::getInstance($row->id, $this->tree); - if ($family->canShow()) { - $total = (int) $row->tot; - - if ($type === 'list') { - $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s grandchild', '%s grandchildren', $total, I18N::number($total)); - } else { - $top10[] = '<a href="' . e($family->url()) . '">' . $family->getFullName() . '</a> - ' . I18N::plural('%s grandchild', '%s grandchildren', $total, I18N::number($total)); - } - } - } - if ($type === 'list') { - $top10 = implode('', $top10); - } else { - $top10 = implode('; ', $top10); - } - if (I18N::direction() === 'rtl') { - $top10 = str_replace([ - '[', - ']', - '(', - ')', - '+', - ], [ - '‏[', - '‏]', - '‏(', - '‏)', - '‏+', - ], $top10); - } - if ($type === 'list') { - return '<ul>' . $top10 . '</ul>'; - } - - return $top10; + public function chartNoChildrenFamilies( + string $size = '220x200', + string $year1 = '-1', + string $year2 = '-1' + ): string { + return $this->familyRepository->chartNoChildrenFamilies($size, (int) $year1, (int) $year2); } /** - * Find the couple with the most grandchildren. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topTenLargestGrandFamily(string $total = '10'): string { - return $this->topTenGrandFamilyQuery('nolist', (int) $total); + return $this->familyRepository->topTenLargestGrandFamily((int) $total); } /** - * Find the couple with the most grandchildren. - * - * @param string $total - * - * @return string + * @inheritDoc */ public function topTenLargestGrandFamilyList(string $total = '10'): string { - return $this->topTenGrandFamilyQuery('list', (int) $total); + return $this->familyRepository->topTenLargestGrandFamilyList((int) $total); } /** - * Find common surnames. - * - * @param string $type - * @param bool $show_tot - * @param int $threshold - * @param int $number_of_surnames - * @param string $sorting - * - * @return string - */ - private function commonSurnamesQuery($type, $show_tot, int $threshold, int $number_of_surnames, string $sorting): string - { - $surnames = $this->topSurnames($number_of_surnames, $threshold); - - switch ($sorting) { - default: - case 'alpha': - uksort($surnames, [I18N::class, 'strcasecmp']); - break; - case 'count': - break; - case 'rcount': - $surnames = array_reverse($surnames, true); - break; - } - - return FunctionsPrintLists::surnameList($surnames, ($type == 'list' ? 1 : 2), $show_tot, 'individual-list', $this->tree); - } - - /** - * @param int $number_of_surnames - * @param int $threshold - * - * @return array - */ - private function topSurnames(int $number_of_surnames, int $threshold): array - { - // Use the count of base surnames. - $top_surnames = Database::prepare( - "SELECT n_surn FROM `##name`" . - " WHERE n_file = :tree_id AND n_type != '_MARNM' AND n_surn NOT IN ('@N.N.', '')" . - " GROUP BY n_surn" . - " ORDER BY COUNT(n_surn) DESC" . - " LIMIT :limit" - )->execute([ - 'tree_id' => $this->tree->id(), - 'limit' => $number_of_surnames, - ])->fetchOneColumn(); - - $surnames = []; - foreach ($top_surnames as $top_surname) { - $variants = Database::prepare( - "SELECT n_surname COLLATE utf8_bin, COUNT(*) FROM `##name` WHERE n_file = :tree_id AND n_surn COLLATE :collate = :surname GROUP BY 1" - )->execute([ - 'collate' => I18N::collation(), - 'surname' => $top_surname, - 'tree_id' => $this->tree->id(), - ])->fetchAssoc(); - - if (array_sum($variants) > $threshold) { - $surnames[$top_surname] = $variants; - } - } - - return $surnames; - } - - /** - * Find common surnames. - * - * @return string + * @inheritDoc */ public function getCommonSurname(): string { - $top_surname = $this->topSurnames(1, 0); - - return implode(', ', $top_surname[0] ?? []); + return $this->individualRepository->getCommonSurname(); } /** - * Find common surnames. - * - * @param string $threshold - * @param string $number_of_surnames - * @param string $sorting - * - * @return string + * @inheritDoc */ - public function commonSurnames(string $threshold = '1', string $number_of_surnames = '10', string $sorting = 'alpha'): string - { - return $this->commonSurnamesQuery('nolist', false, (int) $threshold, (int) $number_of_surnames, $sorting); + public function commonSurnames( + string $threshold = '1', + string $number_of_surnames = '10', + string $sorting = 'alpha' + ): string { + return $this->individualRepository->commonSurnames((int) $threshold, (int) $number_of_surnames, $sorting); } /** - * Find common surnames. - * - * @param string $threshold - * @param string $number_of_surnames - * @param string $sorting - * - * @return string + * @inheritDoc */ - public function commonSurnamesTotals(string $threshold = '1', string $number_of_surnames = '10', string $sorting = 'rcount'): string - { - return $this->commonSurnamesQuery('nolist', true, (int) $threshold, (int) $number_of_surnames, $sorting); + public function commonSurnamesTotals( + string $threshold = '1', + string $number_of_surnames = '10', + string $sorting = 'rcount' + ): string { + return $this->individualRepository->commonSurnamesTotals((int) $threshold, (int) $number_of_surnames, $sorting); } /** - * Find common surnames. - * - * @param string $threshold - * @param string $number_of_surnames - * @param string $sorting - * - * @return string + * @inheritDoc */ - public function commonSurnamesList(string $threshold = '1', string $number_of_surnames = '10', string $sorting = 'alpha'): string - { - return $this->commonSurnamesQuery('list', false, (int) $threshold, (int) $number_of_surnames, $sorting); + public function commonSurnamesList( + string $threshold = '1', + string $number_of_surnames = '10', + string $sorting = 'alpha' + ): string { + return $this->individualRepository->commonSurnamesList((int) $threshold, (int) $number_of_surnames, $sorting); } /** - * Find common surnames. - * - * @param string $threshold - * @param string $number_of_surnames - * @param string $sorting - * - * @return string + * @inheritDoc */ - public function commonSurnamesListTotals(string $threshold = '1', string $number_of_surnames = '10', string $sorting = 'rcount'): string - { - return $this->commonSurnamesQuery('list', true, (int) $threshold, (int) $number_of_surnames, $sorting); + public function commonSurnamesListTotals( + string $threshold = '1', + string $number_of_surnames = '10', + string $sorting = 'rcount' + ): string { + return $this->individualRepository + ->commonSurnamesListTotals((int) $threshold, (int) $number_of_surnames, $sorting); } /** - * Create a chart of common surnames. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * @param string $number_of_surnames - * - * @return string + * @inheritDoc */ - public function chartCommonSurnames(string $size = null, string $color_from = null, string $color_to = null, string $number_of_surnames = '10'): string - { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - $number_of_surnames = (int) $number_of_surnames; - - $sizes = explode('x', $size); - $tot_indi = $this->totalIndividualsQuery(); - - $all_surnames = $this->topSurnames($number_of_surnames, 0); - - if (empty($all_surnames)) { - return ''; - } - - $SURNAME_TRADITION = $this->tree->getPreference('SURNAME_TRADITION'); - - $tot = 0; - - foreach ($all_surnames as $surn => $surnames) { - $tot += array_sum($surnames); - } - - $chd = ''; - $chl = []; - foreach ($all_surnames as $surns) { - $count_per = 0; - $max_name = 0; - $top_name = ''; - foreach ($surns as $spfxsurn => $count) { - $per = $count; - $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([ - '/ska$/', - '/cka$/', - '/dzka$/', - '/żka$/', - ], [ - 'ski', - 'cki', - 'dzki', - 'żki', - ], $top_name); - } - $per = intdiv(100 * $count_per, $tot_indi); - $chd .= $this->arrayToExtendedEncoding([$per]); - $chl[] = $top_name . ' - ' . I18N::number($count_per); - } - $per = intdiv(100 * ($tot_indi - $tot), $tot_indi); - $chd .= $this->arrayToExtendedEncoding([$per]); - $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); - - $chart_title = implode(I18N::$list_separator, $chl); - $chl = implode('|', $chl); - - return '<img src="https://chart.googleapis.com/chart?cht=p3&chd=e:' . $chd . '&chs=' . $size . '&chco=' . $color_from . ',' . $color_to . '&chf=bg,s,ffffff00&chl=' . rawurlencode($chl) . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . $chart_title . '" title="' . $chart_title . '" />'; + public function chartCommonSurnames( + string $size = null, + string $color_from = null, + string $color_to = null, + string $number_of_surnames = '10' + ): string { + return $this->individualRepository + ->chartCommonSurnames($size, $color_from, $color_to, (int) $number_of_surnames); } /** - * Find common given names. - * - * @param string $sex - * @param string $type - * @param bool $show_tot - * @param int $threshold - * @param int $maxtoshow - * - * @return string|int[] - */ - private function commonGivenQuery(string $sex, string $type, bool $show_tot, int $threshold, int $maxtoshow) - { - 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->id(); - - $rows = Database::prepare("SELECT 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 = []; - foreach ($rows as $row) { - $row->num = (int) $row->num; - - // 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] += (int) $row->num; - } else { - $nameList[$given] = (int) $row->num; - } - } - } - } - arsort($nameList); - $nameList = array_slice($nameList, 0, $maxtoshow); - - foreach ($nameList as $given => $total) { - if ($total < $threshold) { - unset($nameList[$given]); - } - } - - switch ($type) { - case 'chart': - return $nameList; - - case 'table': - return view('lists/given-names-table', [ - 'given_names' => $nameList, - ]); - - case 'list': - return view('lists/given-names-list', [ - 'given_names' => $nameList, - 'show_totals' => $show_tot, - ]); - - case 'nolist': - default: - array_walk($nameList, function (int &$value, string $key) use ($show_tot): void { - if ($show_tot) { - $value = '<span dir="auto">' . e($key); - } else { - $value = '<span dir="auto">' . e($key) . ' (' . I18N::number($value) . ')'; - } - }); - - return implode(I18N::$list_separator, $nameList); - } - } - - /** - * Find common give names. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGiven(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('B', 'nolist', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGiven((int) $threshold, (int) $maxtoshow); } /** - * Find common give names. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('B', 'nolist', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenList(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('B', 'list', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenList((int) $threshold, (int) $maxtoshow); } /** - * Find common give names. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenListTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('B', 'list', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenListTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenTable(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('B', 'table', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenTable((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of females. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenFemale(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('F', 'nolist', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenFemale((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of females. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenFemaleTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('F', 'nolist', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenFemaleTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of females. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenFemaleList(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('F', 'list', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenFemaleList((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of females. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenFemaleListTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('F', 'list', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenFemaleListTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of females. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenFemaleTable(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('F', 'table', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenFemaleTable((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of males. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenMale(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('M', 'nolist', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenMale((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of males. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenMaleTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('M', 'nolist', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenMaleTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of males. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenMaleList(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('M', 'list', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenMaleList((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of males. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenMaleListTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('M', 'list', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenMaleListTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of males. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenMaleTable(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('M', 'table', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenMaleTable((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of unknown sexes. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenUnknown(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('U', 'nolist', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenUnknown((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of unknown sexes. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenUnknownTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('U', 'nolist', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenUnknownTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of unknown sexes. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenUnknownList(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('U', 'list', false, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenUnknownList((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of unknown sexes. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenUnknownListTotals(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('U', 'list', true, (int) $threshold, (int) $maxtoshow); + return $this->individualRepository->commonGivenUnknownListTotals((int) $threshold, (int) $maxtoshow); } /** - * Find common give names of unknown sexes. - * - * @param string $threshold - * @param string $maxtoshow - * - * @return string + * @inheritDoc */ public function commonGivenUnknownTable(string $threshold = '1', string $maxtoshow = '10'): string { - return $this->commonGivenQuery('U', 'table', false, (int) $threshold, (int) $maxtoshow); - } - - /** - * Create a chart of common given names. - * - * @param string|null $size - * @param string|null $color_from - * @param string|null $color_to - * @param string $maxtoshow - * - * @return string - */ - public function chartCommonGiven(string $size = null, string $color_from = null, string $color_to = null, string $maxtoshow = '7'): string - { - $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 = $size ?? ($WT_STATS_S_CHART_X . 'x' . $WT_STATS_S_CHART_Y); - $color_from = $color_from ?? $WT_STATS_CHART_COLOR1; - $color_to = $color_to ?? $WT_STATS_CHART_COLOR2; - $maxtoshow = (int) $maxtoshow; - - $sizes = explode('x', $size); - $tot_indi = $this->totalIndividualsQuery(); - $given = $this->commonGivenQuery('B', 'chart', false, 1, (int) $maxtoshow); - - if (empty($given)) { - return ''; - } - - $tot = 0; - foreach ($given as $count) { - $tot += $count; - } - $chd = ''; - $chl = []; - foreach ($given as $givn => $count) { - if ($tot == 0) { - $per = 0; - } else { - $per = intdiv(100 * $count, $tot_indi); - } - $chd .= $this->arrayToExtendedEncoding([$per]); - $chl[] = $givn . ' - ' . I18N::number($count); - } - $per = intdiv(100 * ($tot_indi - $tot), $tot_indi); - $chd .= $this->arrayToExtendedEncoding([$per]); - $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); - - $chart_title = implode(I18N::$list_separator, $chl); - $chl = implode('|', $chl); - - return "<img src=\"https://chart.googleapis.com/chart?cht=p3&chd=e:{$chd}&chs={$size}&chco={$color_from},{$color_to}&chf=bg,s,ffffff00&chl=" . rawurlencode($chl) . "\" width=\"{$sizes[0]}\" height=\"{$sizes[1]}\" alt=\"" . $chart_title . '" title="' . $chart_title . '" />'; - } - - /** - * Who is currently logged in? - * - * @TODO - this is duplicated from the LoggedInUsersModule class. - * - * @param string $type - * - * @return string - */ - private function usersLoggedInQuery($type = 'nolist'): string - { - $content = ''; - // List active users - $NumAnonymous = 0; - $loggedusers = []; - 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 .= '<b>' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $NumAnonymous, I18N::number($NumAnonymous)) . '</b>'; - } - if ($LoginUsers > 0) { - if ($NumAnonymous) { - if ($type == 'list') { - $content .= '<br><br>'; - } else { - $content .= ' ' . I18N::translate('and') . ' '; - } - } - $content .= '<b>' . I18N::plural('%s signed-in user', '%s signed-in users', $LoginUsers, I18N::number($LoginUsers)) . '</b>'; - if ($type == 'list') { - $content .= '<ul>'; - } else { - $content .= ': '; - } - } - if (Auth::check()) { - foreach ($loggedusers as $user) { - if ($type == 'list') { - $content .= '<li>' . e($user->getRealName()) . ' - ' . e($user->getUserName()); - } else { - $content .= e($user->getRealName()) . ' - ' . e($user->getUserName()); - } - if (Auth::id() !== $user->id() && $user->getPreference('contactmethod') !== 'none') { - if ($type == 'list') { - $content .= '<br>'; - } - $content .= '<a href="' . e(route('message', ['to' => $user->getUserName(), 'ged' => $this->tree->name()])) . '" class="btn btn-link" title="' . I18N::translate('Send a message') . '">' . view('icons/email') . '</a>'; - } - if ($type == 'list') { - $content .= '</li>'; - } - } - } - if ($type == 'list') { - $content .= '</ul>'; - } - - return $content; + return $this->individualRepository->commonGivenUnknownTable((int) $threshold, (int) $maxtoshow); } /** - * NUmber of users who are currently logged in? - * - * @param string $type - * - * @return int + * @inheritDoc */ - private function usersLoggedInTotalQuery($type = 'all'): int - { - $anon = 0; - $visible = 0; - foreach (User::allLoggedIn() as $user) { - if (Auth::isAdmin() || $user->getPreference('visibleonline')) { - $visible++; - } else { - $anon++; - } - } - if ($type == 'anon') { - return $anon; - } - - if ($type == 'visible') { - return $visible; - } - - return $visible + $anon; + public function chartCommonGiven( + string $size = null, + string $color_from = null, + string $color_to = null, + string $maxtoshow = '7' + ): string { + return $this->individualRepository->chartCommonGiven($size, $color_from, $color_to, (int) $maxtoshow); } /** - * Who is currently logged in? - * - * @return string + * @inheritDoc */ public function usersLoggedIn(): string { - return $this->usersLoggedInQuery('nolist'); + return $this->userRepository->usersLoggedIn(); } /** - * Who is currently logged in? - * - * @return string + * @inheritDoc */ public function usersLoggedInList(): string { - return $this->usersLoggedInQuery('list'); + return $this->userRepository->usersLoggedInList(); } /** - * Who is currently logged in? - * - * @return int + * @inheritDoc */ public function usersLoggedInTotal(): int { - return $this->usersLoggedInTotalQuery('all'); + return $this->userRepository->usersLoggedInTotal(); } /** - * Which visitors are currently logged in? - * - * @return int + * @inheritDoc */ public function usersLoggedInTotalAnon(): int { - return $this->usersLoggedInTotalQuery('anon'); + return $this->userRepository->usersLoggedInTotalAnon(); } /** - * Which visitors are currently logged in? - * - * @return int + * @inheritDoc */ public function usersLoggedInTotalVisible(): int { - return $this->usersLoggedInTotalQuery('visible'); + return $this->userRepository->usersLoggedInTotalVisible(); } /** - * Get the current user's ID. - * - * @return string + * @inheritDoc */ public function userId(): string { - return (string) Auth::id(); + return $this->userRepository->userId(); } /** - * Get the current user's username. - * - * @param string $visitor_text - * - * @return string + * @inheritDoc */ public function userName(string $visitor_text = ''): string { - if (Auth::check()) { - return e(Auth::user()->getUserName()); - } - - // if #username:visitor# was specified, then "visitor" will be returned when the user is not logged in - return e($visitor_text); + return $this->userRepository->userName(); } /** - * Get the current user's full name. - * - * @return string + * @inheritDoc */ public function userFullName(): string { - return Auth::check() ? '<span dir="auto">' . e(Auth::user()->getRealName()) . '</span>' : ''; + return $this->userRepository->userFullName(); } /** - * Find the newest user on the site. - * If no user has registered (i.e. all created by the admin), then - * return the current user. - * - * @return User + * @inheritDoc */ - private function latestUser(): User + public function totalUsers(): string { - static $user; - - if (!$user instanceof User) { - $user_id = (int) Database::prepare( - "SELECT u.user_id" . - " FROM `##user` u" . - " LEFT JOIN `##user_setting` us ON (u.user_id=us.user_id AND us.setting_name='reg_timestamp') " . - " ORDER BY us.setting_value DESC LIMIT 1" - )->execute()->fetchOne(); + return $this->userRepository->totalUsers(); + } - $user = User::find($user_id) ?? Auth::user(); - } + /** + * @inheritDoc + */ + public function totalAdmins(): string + { + return $this->userRepository->totalAdmins(); + } - return $user; + /** + * @inheritDoc + */ + public function totalNonAdmins(): string + { + return $this->userRepository->totalNonAdmins(); } /** - * Get the newest registered user's ID. - * - * @return string + * @inheritDoc */ public function latestUserId(): string { - return (string) $this->latestUser()->id(); + return $this->latestUserRepository->latestUserId(); } /** - * Get the newest registered user's username. - * - * @return string + * @inheritDoc */ public function latestUserName(): string { - return e($this->latestUser()->getUserName()); + return $this->latestUserRepository->latestUserName(); } /** - * Get the newest registered user's real name. - * - * @return string + * @inheritDoc */ public function latestUserFullName(): string { - return e($this->latestUser()->getRealName()); + return $this->latestUserRepository->latestUserFullName(); } /** - * Get the date of the newest user registration. - * - * @param string|null $format - * - * @return string + * @inheritDoc */ public function latestUserRegDate(string $format = null): string { - $format = $format ?? I18N::dateFormat(); - - $user = $this->latestUser(); - - return FunctionsDate::timestampToGedcomDate((int) $user->getPreference('reg_timestamp'))->display(false, $format); + return $this->latestUserRepository->latestUserRegDate(); } /** - * Find the timestamp of the latest user to register. - * - * @param string|null $format - * - * @return string + * @inheritDoc */ public function latestUserRegTime(string $format = null): string { - $format = $format ?? str_replace('%', '', I18N::timeFormat()); - - $user = $this->latestUser(); - - return date($format, (int) $user->getPreference('reg_timestamp')); + return $this->latestUserRepository->latestUserRegTime(); } /** - * Is the most recently registered user logged in right now? - * - * @param string|null $yes - * @param string|null $no - * - * @return string + * @inheritDoc */ public function latestUserLoggedin(string $yes = null, string $no = null): string { - $yes = $yes ?? I18N::translate('yes'); - $no = $no ?? I18N::translate('no'); - - $user = $this->latestUser(); - - $is_logged_in = (bool) Database::prepare( - "SELECT 1 FROM `##session` WHERE user_id = :user_id LIMIT 1" - )->execute([ - 'user_id' => $user->id(), - ])->fetchOne(); - - return $is_logged_in ? $yes : $no; + return $this->latestUserRepository->latestUserLoggedin(); } /** - * Create a link to contact the webmaster. - * - * @return string + * @inheritDoc */ - public function contactWebmaster() + public function contactWebmaster(): string { - $user_id = $this->tree->getPreference('WEBMASTER_USER_ID'); - $user = User::find((int) $user_id); - - if ($user instanceof User) { - return view('modules/contact-links/contact', [ - 'request' => app()->make(Request::class), - 'user' => $user, - 'tree' => $this->tree, - ]); - } - - return ''; + return $this->contactRepository->contactWebmaster(); } /** - * Create a link to contact the genealogy contact. - * - * @return string + * @inheritDoc */ - public function contactGedcom() + public function contactGedcom(): string { - $user_id = $this->tree->getPreference('CONTACT_USER_ID'); - $user = User::find((int) $user_id); - - if ($user instanceof User) { - return view('modules/contact-links/contact', [ - 'request' => app()->make(Request::class), - 'user' => $user, - 'tree' => $this->tree, - ]); - } - - return ''; + return $this->contactRepository->contactGedcom(); } /** - * What is the current date on the server? - * - * @return string + * @inheritDoc */ public function serverDate(): string { - return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display(); + return $this->serverRepository->serverDate(); } /** - * What is the current time on the server (in 12 hour clock)? - * - * @return string + * @inheritDoc */ public function serverTime(): string { - return date('g:i a'); + return $this->serverRepository->serverTime(); } /** - * What is the current time on the server (in 24 hour clock)? - * - * @return string + * @inheritDoc */ public function serverTime24(): string { - return date('G:i'); + return $this->serverRepository->serverTime24(); } /** @@ -6417,1213 +2419,198 @@ class Stats */ public function serverTimezone(): string { - return date('T'); + return $this->serverRepository->serverTimezone(); } /** - * What is the client's date. - * - * @return string + * @inheritDoc */ public function browserDate(): string { - return FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP)->display(); + return $this->browserRepository->browserDate(); } /** - * What is the client's timestamp. - * - * @return string + * @inheritDoc */ public function browserTime(): string { - return date(str_replace('%', '', I18N::timeFormat()), WT_TIMESTAMP + WT_TIMESTAMP_OFFSET); + return $this->browserRepository->browserTime(); } /** - * What is the browser's tiemzone. - * - * @return string + * @inheritDoc */ public function browserTimezone(): string { - return date('T', WT_TIMESTAMP + WT_TIMESTAMP_OFFSET); - } - - /** - * What is the current version of webtrees. - * - * @return string - */ - public function webtreesVersion(): string - { - return Webtrees::VERSION; + return $this->browserRepository->browserTimezone(); } /** - * These functions provide access to hitcounter for use in the HTML block. - * - * @param string $page_name - * @param string $page_parameter - * - * @return string - */ - private function hitCountQuery($page_name, string $page_parameter = ''): string - { - if ($page_name === '') { - // index.php?ctype=gedcom - $page_name = 'index.php'; - $page_parameter = 'gedcom:' . $this->tree->id(); - } elseif ($page_name == 'index.php') { - // index.php?ctype=user - $user = User::findByIdentifier($page_parameter); - $page_parameter = 'user:' . ($user ? $user->id() : Auth::id()); - } - - $count = (int) DB::table('hit_counter') - ->where('gedcom_id', '=', $this->tree->id()) - ->where('page_name', '=', $page_name) - ->where('page_parameter', '=', $page_parameter) - ->value('page_count'); - - return '<span class="odometer">' . I18N::digits($count) . '</span>'; - } - - /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCount(string $page_parameter = ''): string { - return $this->hitCountQuery('', $page_parameter); + return $this->hitCountRepository->hitCount($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountUser(string $page_parameter = ''): string { - return $this->hitCountQuery('index.php', $page_parameter); + return $this->hitCountRepository->hitCountUser($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountIndi(string $page_parameter = ''): string { - return $this->hitCountQuery('individual.php', $page_parameter); + return $this->hitCountRepository->hitCountIndi($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountFam(string $page_parameter = ''): string { - return $this->hitCountQuery('family.php', $page_parameter); + return $this->hitCountRepository->hitCountFam($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountSour(string $page_parameter = ''): string { - return $this->hitCountQuery('source.php', $page_parameter); + return $this->hitCountRepository->hitCountSour($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountRepo(string $page_parameter = ''): string { - return $this->hitCountQuery('repo.php', $page_parameter); + return $this->hitCountRepository->hitCountRepo($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountNote(string $page_parameter = ''): string { - return $this->hitCountQuery('note.php', $page_parameter); + return $this->hitCountRepository->hitCountNote($page_parameter); } /** - * How many times has a page been viewed. - * - * @param string $page_parameter - * - * @return string + * @inheritDoc */ public function hitCountObje(string $page_parameter = ''): string { - return $this->hitCountQuery('mediaviewer.php', $page_parameter); + return $this->hitCountRepository->hitCountObje($page_parameter); } /** - * Convert numbers to Google's custom encoding. - * - * @link http://bendodson.com/news/google-extended-encoding-made-easy - * - * @param int[] $a - * - * @return string + * @inheritDoc */ - private function arrayToExtendedEncoding($a): string + public function gedcomFavorites(): string { - $xencoding = self::GOOGLE_CHART_ENCODING; - - $encoding = ''; - foreach ($a as $value) { - if ($value < 0) { - $value = 0; - } - $first = intdiv($value, 64); - $second = $value % 64; - $encoding .= $xencoding[(int) $first] . $xencoding[(int) $second]; - } - - return $encoding; + return $this->favoritesRepository->gedcomFavorites(); } /** - * Run an SQL query and cache the result. - * - * @param string $sql - * - * @return stdClass[] + * @inheritDoc */ - private function runSql($sql): array + public function userFavorites(): string { - static $cache = []; - - $id = md5($sql); - if (isset($cache[$id])) { - return $cache[$id]; - } - $rows = Database::prepare($sql)->fetchAll(); - $cache[$id] = $rows; - - return $rows; + return $this->favoritesRepository->userFavorites(); } /** - * Find the favorites for the tree. - * - * @return string + * @inheritDoc */ - public function gedcomFavorites(): string + public function totalGedcomFavorites(): string { - $module = Module::findByClass(FamilyTreeFavoritesModule::class); - - if ($module instanceof FamilyTreeFavoritesModule) { - return $module->getBlock($this->tree, 0, ''); - } - - return ''; + return $this->favoritesRepository->totalGedcomFavorites(); } /** - * Find the favorites for the user. - * - * @return string + * @inheritDoc */ - public function userFavorites(): string + public function totalUserFavorites(): string { - $module = Module::findByClass(UserFavoritesModule::class); - - if ($module instanceof UserFavoritesModule) { - return $module->getBlock($this->tree, 0, ''); - } - - return ''; + return $this->favoritesRepository->totalUserFavorites(); } /** - * Find the number of favorites for the tree. - * - * @return string + * @inheritDoc */ - public function totalGedcomFavorites(): string + public function totalUserMessages(): string { - $count = 0; - - $module = Module::findByClass(FamilyTreeFavoritesModule::class); - - if ($module instanceof FamilyTreeFavoritesModule) { - $count = count($module->getFavorites($this->tree)); - } - - return I18N::number($count); + return $this->messageRepository->totalUserMessages(); } /** - * Find the number of favorites for the user. - * - * @return string + * @inheritDoc */ - public function totalUserFavorites(): string + public function totalUserJournal(): string { - $count = 0; - - $module = Module::findByClass(UserFavoritesModule::class); - - if ($module instanceof UserFavoritesModule) { - $count = count($module->getFavorites($this->tree, Auth::user())); - } + return $this->newsRepository->totalUserJournal(); + } - return I18N::number($count); + /** + * @inheritDoc + */ + public function totalGedcomNews(): string + { + return $this->newsRepository->totalGedcomNews(); } /** * Create any of the other blocks. * Use as #callBlock:block_name# * - * @param string $block_name + * @param string $block * @param string ...$params * - * @return string + * @return null|string */ - public function callBlock(string $block_name = '', ...$params): string + public function callBlock(string $block = '', ...$params): ?string { - /** @var ModuleBlockInterface $block */ - $block = Module::findByComponent('block', $this->tree, Auth::user()) - ->filter(function (ModuleInterface $block) use ($block_name): bool { - return $block->name() === $block_name && $block->name() !== 'html'; + /** @var ModuleBlockInterface $module */ + $module = Module::findByComponent('block', $this->tree, Auth::user()) + ->filter(function (ModuleInterface $module) use ($block): bool { + return $module->name() === $block && $module->name() !== 'html'; }) ->first(); - if ($block === null) { + if ($module === null) { return ''; } + // Build the config array $cfg = []; foreach ($params as $config) { $bits = explode('=', $config); - if (count($bits) < 2) { + + if (\count($bits) < 2) { continue; } + $v = array_shift($bits); $cfg[$v] = implode('=', $bits); } - $content = $block->getBlock($this->tree, 0, '', $cfg); - - return $content; - } - - /** - * How many messages in the user's inbox. - * - * @return string - */ - public function totalUserMessages(): string - { - $total = (int) Database::prepare("SELECT COUNT(*) FROM `##message` WHERE user_id = ?") - ->execute([Auth::id()]) - ->fetchOne(); - - return I18N::number($total); - } - - /** - * How many blog entries exist for this user. - * - * @return string - */ - public function totalUserJournal(): string - { - try { - $number = (int) Database::prepare("SELECT COUNT(*) FROM `##news` WHERE user_id = ?") - ->execute([Auth::id()]) - ->fetchOne(); - } catch (PDOException $ex) { - // The module may not be installed, so the table may not exist. - $number = 0; - } - - return I18N::number($number); + return $module->getBlock($this->tree, 0, '', $cfg); } /** - * How many news items exist for this tree. - * - * @return string - */ - public function totalGedcomNews(): string - { - try { - $number = (int) Database::prepare("SELECT COUNT(*) FROM `##news` WHERE gedcom_id = ?") - ->execute([$this->tree->id()]) - ->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(): array - { - return [ - '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(): array - { - return [ - /* I18N: Name of a country or state */ - '???' => I18N::translate('Unknown'), - /* I18N: Name of a country or state */ - 'ABW' => I18N::translate('Aruba'), - /* I18N: Name of a country or state */ - 'AFG' => I18N::translate('Afghanistan'), - /* I18N: Name of a country or state */ - 'AGO' => I18N::translate('Angola'), - /* I18N: Name of a country or state */ - 'AIA' => I18N::translate('Anguilla'), - /* I18N: Name of a country or state */ - 'ALA' => I18N::translate('Aland Islands'), - /* I18N: Name of a country or state */ - 'ALB' => I18N::translate('Albania'), - /* I18N: Name of a country or state */ - 'AND' => I18N::translate('Andorra'), - /* I18N: Name of a country or state */ - 'ARE' => I18N::translate('United Arab Emirates'), - /* I18N: Name of a country or state */ - 'ARG' => I18N::translate('Argentina'), - /* I18N: Name of a country or state */ - 'ARM' => I18N::translate('Armenia'), - /* I18N: Name of a country or state */ - 'ASM' => I18N::translate('American Samoa'), - /* I18N: Name of a country or state */ - 'ATA' => I18N::translate('Antarctica'), - /* I18N: Name of a country or state */ - 'ATF' => I18N::translate('French Southern Territories'), - /* I18N: Name of a country or state */ - 'ATG' => I18N::translate('Antigua and Barbuda'), - /* I18N: Name of a country or state */ - 'AUS' => I18N::translate('Australia'), - /* I18N: Name of a country or state */ - 'AUT' => I18N::translate('Austria'), - /* I18N: Name of a country or state */ - 'AZE' => I18N::translate('Azerbaijan'), - /* I18N: Name of a country or state */ - 'AZR' => I18N::translate('Azores'), - /* I18N: Name of a country or state */ - 'BDI' => I18N::translate('Burundi'), - /* I18N: Name of a country or state */ - 'BEL' => I18N::translate('Belgium'), - /* I18N: Name of a country or state */ - 'BEN' => I18N::translate('Benin'), - // BES => Bonaire, Sint Eustatius and Saba - /* I18N: Name of a country or state */ - 'BFA' => I18N::translate('Burkina Faso'), - /* I18N: Name of a country or state */ - 'BGD' => I18N::translate('Bangladesh'), - /* I18N: Name of a country or state */ - 'BGR' => I18N::translate('Bulgaria'), - /* I18N: Name of a country or state */ - 'BHR' => I18N::translate('Bahrain'), - /* I18N: Name of a country or state */ - 'BHS' => I18N::translate('Bahamas'), - /* I18N: Name of a country or state */ - 'BIH' => I18N::translate('Bosnia and Herzegovina'), - // BLM => Saint Barthélemy - /* I18N: Name of a country or state */ - 'BLR' => I18N::translate('Belarus'), - /* I18N: Name of a country or state */ - 'BLZ' => I18N::translate('Belize'), - /* I18N: Name of a country or state */ - 'BMU' => I18N::translate('Bermuda'), - /* I18N: Name of a country or state */ - 'BOL' => I18N::translate('Bolivia'), - /* I18N: Name of a country or state */ - 'BRA' => I18N::translate('Brazil'), - /* I18N: Name of a country or state */ - 'BRB' => I18N::translate('Barbados'), - /* I18N: Name of a country or state */ - 'BRN' => I18N::translate('Brunei Darussalam'), - /* I18N: Name of a country or state */ - 'BTN' => I18N::translate('Bhutan'), - /* I18N: Name of a country or state */ - 'BVT' => I18N::translate('Bouvet Island'), - /* I18N: Name of a country or state */ - 'BWA' => I18N::translate('Botswana'), - /* I18N: Name of a country or state */ - 'CAF' => I18N::translate('Central African Republic'), - /* I18N: Name of a country or state */ - 'CAN' => I18N::translate('Canada'), - /* I18N: Name of a country or state */ - 'CCK' => I18N::translate('Cocos (Keeling) Islands'), - /* I18N: Name of a country or state */ - 'CHE' => I18N::translate('Switzerland'), - /* I18N: Name of a country or state */ - 'CHL' => I18N::translate('Chile'), - /* I18N: Name of a country or state */ - 'CHN' => I18N::translate('China'), - /* I18N: Name of a country or state */ - 'CIV' => I18N::translate('Cote d’Ivoire'), - /* I18N: Name of a country or state */ - 'CMR' => I18N::translate('Cameroon'), - /* I18N: Name of a country or state */ - 'COD' => I18N::translate('Democratic Republic of the Congo'), - /* I18N: Name of a country or state */ - 'COG' => I18N::translate('Republic of the Congo'), - /* I18N: Name of a country or state */ - 'COK' => I18N::translate('Cook Islands'), - /* I18N: Name of a country or state */ - 'COL' => I18N::translate('Colombia'), - /* I18N: Name of a country or state */ - 'COM' => I18N::translate('Comoros'), - /* I18N: Name of a country or state */ - 'CPV' => I18N::translate('Cape Verde'), - /* I18N: Name of a country or state */ - 'CRI' => I18N::translate('Costa Rica'), - /* I18N: Name of a country or state */ - 'CUB' => I18N::translate('Cuba'), - // CUW => Curaçao - /* I18N: Name of a country or state */ - 'CXR' => I18N::translate('Christmas Island'), - /* I18N: Name of a country or state */ - 'CYM' => I18N::translate('Cayman Islands'), - /* I18N: Name of a country or state */ - 'CYP' => I18N::translate('Cyprus'), - /* I18N: Name of a country or state */ - 'CZE' => I18N::translate('Czech Republic'), - /* I18N: Name of a country or state */ - 'DEU' => I18N::translate('Germany'), - /* I18N: Name of a country or state */ - 'DJI' => I18N::translate('Djibouti'), - /* I18N: Name of a country or state */ - 'DMA' => I18N::translate('Dominica'), - /* I18N: Name of a country or state */ - 'DNK' => I18N::translate('Denmark'), - /* I18N: Name of a country or state */ - 'DOM' => I18N::translate('Dominican Republic'), - /* I18N: Name of a country or state */ - 'DZA' => I18N::translate('Algeria'), - /* I18N: Name of a country or state */ - 'ECU' => I18N::translate('Ecuador'), - /* I18N: Name of a country or state */ - 'EGY' => I18N::translate('Egypt'), - /* I18N: Name of a country or state */ - 'ENG' => I18N::translate('England'), - /* I18N: Name of a country or state */ - 'ERI' => I18N::translate('Eritrea'), - /* I18N: Name of a country or state */ - 'ESH' => I18N::translate('Western Sahara'), - /* I18N: Name of a country or state */ - 'ESP' => I18N::translate('Spain'), - /* I18N: Name of a country or state */ - 'EST' => I18N::translate('Estonia'), - /* I18N: Name of a country or state */ - 'ETH' => I18N::translate('Ethiopia'), - /* I18N: Name of a country or state */ - 'FIN' => I18N::translate('Finland'), - /* I18N: Name of a country or state */ - 'FJI' => I18N::translate('Fiji'), - /* I18N: Name of a country or state */ - 'FLD' => I18N::translate('Flanders'), - /* I18N: Name of a country or state */ - 'FLK' => I18N::translate('Falkland Islands'), - /* I18N: Name of a country or state */ - 'FRA' => I18N::translate('France'), - /* I18N: Name of a country or state */ - 'FRO' => I18N::translate('Faroe Islands'), - /* I18N: Name of a country or state */ - 'FSM' => I18N::translate('Micronesia'), - /* I18N: Name of a country or state */ - 'GAB' => I18N::translate('Gabon'), - /* I18N: Name of a country or state */ - 'GBR' => I18N::translate('United Kingdom'), - /* I18N: Name of a country or state */ - 'GEO' => I18N::translate('Georgia'), - /* I18N: Name of a country or state */ - 'GGY' => I18N::translate('Guernsey'), - /* I18N: Name of a country or state */ - 'GHA' => I18N::translate('Ghana'), - /* I18N: Name of a country or state */ - 'GIB' => I18N::translate('Gibraltar'), - /* I18N: Name of a country or state */ - 'GIN' => I18N::translate('Guinea'), - /* I18N: Name of a country or state */ - 'GLP' => I18N::translate('Guadeloupe'), - /* I18N: Name of a country or state */ - 'GMB' => I18N::translate('Gambia'), - /* I18N: Name of a country or state */ - 'GNB' => I18N::translate('Guinea-Bissau'), - /* I18N: Name of a country or state */ - 'GNQ' => I18N::translate('Equatorial Guinea'), - /* I18N: Name of a country or state */ - 'GRC' => I18N::translate('Greece'), - /* I18N: Name of a country or state */ - 'GRD' => I18N::translate('Grenada'), - /* I18N: Name of a country or state */ - 'GRL' => I18N::translate('Greenland'), - /* I18N: Name of a country or state */ - 'GTM' => I18N::translate('Guatemala'), - /* I18N: Name of a country or state */ - 'GUF' => I18N::translate('French Guiana'), - /* I18N: Name of a country or state */ - 'GUM' => I18N::translate('Guam'), - /* I18N: Name of a country or state */ - 'GUY' => I18N::translate('Guyana'), - /* I18N: Name of a country or state */ - 'HKG' => I18N::translate('Hong Kong'), - /* I18N: Name of a country or state */ - 'HMD' => I18N::translate('Heard Island and McDonald Islands'), - /* I18N: Name of a country or state */ - 'HND' => I18N::translate('Honduras'), - /* I18N: Name of a country or state */ - 'HRV' => I18N::translate('Croatia'), - /* I18N: Name of a country or state */ - 'HTI' => I18N::translate('Haiti'), - /* I18N: Name of a country or state */ - 'HUN' => I18N::translate('Hungary'), - /* I18N: Name of a country or state */ - 'IDN' => I18N::translate('Indonesia'), - /* I18N: Name of a country or state */ - 'IND' => I18N::translate('India'), - /* I18N: Name of a country or state */ - 'IOM' => I18N::translate('Isle of Man'), - /* I18N: Name of a country or state */ - 'IOT' => I18N::translate('British Indian Ocean Territory'), - /* I18N: Name of a country or state */ - 'IRL' => I18N::translate('Ireland'), - /* I18N: Name of a country or state */ - 'IRN' => I18N::translate('Iran'), - /* I18N: Name of a country or state */ - 'IRQ' => I18N::translate('Iraq'), - /* I18N: Name of a country or state */ - 'ISL' => I18N::translate('Iceland'), - /* I18N: Name of a country or state */ - 'ISR' => I18N::translate('Israel'), - /* I18N: Name of a country or state */ - 'ITA' => I18N::translate('Italy'), - /* I18N: Name of a country or state */ - 'JAM' => I18N::translate('Jamaica'), - //'JEY' => Jersey - /* I18N: Name of a country or state */ - 'JOR' => I18N::translate('Jordan'), - /* I18N: Name of a country or state */ - 'JPN' => I18N::translate('Japan'), - /* I18N: Name of a country or state */ - 'KAZ' => I18N::translate('Kazakhstan'), - /* I18N: Name of a country or state */ - 'KEN' => I18N::translate('Kenya'), - /* I18N: Name of a country or state */ - 'KGZ' => I18N::translate('Kyrgyzstan'), - /* I18N: Name of a country or state */ - 'KHM' => I18N::translate('Cambodia'), - /* I18N: Name of a country or state */ - 'KIR' => I18N::translate('Kiribati'), - /* I18N: Name of a country or state */ - 'KNA' => I18N::translate('Saint Kitts and Nevis'), - /* I18N: Name of a country or state */ - 'KOR' => I18N::translate('Korea'), - /* I18N: Name of a country or state */ - 'KWT' => I18N::translate('Kuwait'), - /* I18N: Name of a country or state */ - 'LAO' => I18N::translate('Laos'), - /* I18N: Name of a country or state */ - 'LBN' => I18N::translate('Lebanon'), - /* I18N: Name of a country or state */ - 'LBR' => I18N::translate('Liberia'), - /* I18N: Name of a country or state */ - 'LBY' => I18N::translate('Libya'), - /* I18N: Name of a country or state */ - 'LCA' => I18N::translate('Saint Lucia'), - /* I18N: Name of a country or state */ - 'LIE' => I18N::translate('Liechtenstein'), - /* I18N: Name of a country or state */ - 'LKA' => I18N::translate('Sri Lanka'), - /* I18N: Name of a country or state */ - 'LSO' => I18N::translate('Lesotho'), - /* I18N: Name of a country or state */ - 'LTU' => I18N::translate('Lithuania'), - /* I18N: Name of a country or state */ - 'LUX' => I18N::translate('Luxembourg'), - /* I18N: Name of a country or state */ - 'LVA' => I18N::translate('Latvia'), - /* I18N: Name of a country or state */ - 'MAC' => I18N::translate('Macau'), - // MAF => Saint Martin - /* I18N: Name of a country or state */ - 'MAR' => I18N::translate('Morocco'), - /* I18N: Name of a country or state */ - 'MCO' => I18N::translate('Monaco'), - /* I18N: Name of a country or state */ - 'MDA' => I18N::translate('Moldova'), - /* I18N: Name of a country or state */ - 'MDG' => I18N::translate('Madagascar'), - /* I18N: Name of a country or state */ - 'MDV' => I18N::translate('Maldives'), - /* I18N: Name of a country or state */ - 'MEX' => I18N::translate('Mexico'), - /* I18N: Name of a country or state */ - 'MHL' => I18N::translate('Marshall Islands'), - /* I18N: Name of a country or state */ - 'MKD' => I18N::translate('Macedonia'), - /* I18N: Name of a country or state */ - 'MLI' => I18N::translate('Mali'), - /* I18N: Name of a country or state */ - 'MLT' => I18N::translate('Malta'), - /* I18N: Name of a country or state */ - 'MMR' => I18N::translate('Myanmar'), - /* I18N: Name of a country or state */ - 'MNG' => I18N::translate('Mongolia'), - /* I18N: Name of a country or state */ - 'MNP' => I18N::translate('Northern Mariana Islands'), - /* I18N: Name of a country or state */ - 'MNT' => I18N::translate('Montenegro'), - /* I18N: Name of a country or state */ - 'MOZ' => I18N::translate('Mozambique'), - /* I18N: Name of a country or state */ - 'MRT' => I18N::translate('Mauritania'), - /* I18N: Name of a country or state */ - 'MSR' => I18N::translate('Montserrat'), - /* I18N: Name of a country or state */ - 'MTQ' => I18N::translate('Martinique'), - /* I18N: Name of a country or state */ - 'MUS' => I18N::translate('Mauritius'), - /* I18N: Name of a country or state */ - 'MWI' => I18N::translate('Malawi'), - /* I18N: Name of a country or state */ - 'MYS' => I18N::translate('Malaysia'), - /* I18N: Name of a country or state */ - 'MYT' => I18N::translate('Mayotte'), - /* I18N: Name of a country or state */ - 'NAM' => I18N::translate('Namibia'), - /* I18N: Name of a country or state */ - 'NCL' => I18N::translate('New Caledonia'), - /* I18N: Name of a country or state */ - 'NER' => I18N::translate('Niger'), - /* I18N: Name of a country or state */ - 'NFK' => I18N::translate('Norfolk Island'), - /* I18N: Name of a country or state */ - 'NGA' => I18N::translate('Nigeria'), - /* I18N: Name of a country or state */ - 'NIC' => I18N::translate('Nicaragua'), - /* I18N: Name of a country or state */ - 'NIR' => I18N::translate('Northern Ireland'), - /* I18N: Name of a country or state */ - 'NIU' => I18N::translate('Niue'), - /* I18N: Name of a country or state */ - 'NLD' => I18N::translate('Netherlands'), - /* I18N: Name of a country or state */ - 'NOR' => I18N::translate('Norway'), - /* I18N: Name of a country or state */ - 'NPL' => I18N::translate('Nepal'), - /* I18N: Name of a country or state */ - 'NRU' => I18N::translate('Nauru'), - /* I18N: Name of a country or state */ - 'NZL' => I18N::translate('New Zealand'), - /* I18N: Name of a country or state */ - 'OMN' => I18N::translate('Oman'), - /* I18N: Name of a country or state */ - 'PAK' => I18N::translate('Pakistan'), - /* I18N: Name of a country or state */ - 'PAN' => I18N::translate('Panama'), - /* I18N: Name of a country or state */ - 'PCN' => I18N::translate('Pitcairn'), - /* I18N: Name of a country or state */ - 'PER' => I18N::translate('Peru'), - /* I18N: Name of a country or state */ - 'PHL' => I18N::translate('Philippines'), - /* I18N: Name of a country or state */ - 'PLW' => I18N::translate('Palau'), - /* I18N: Name of a country or state */ - 'PNG' => I18N::translate('Papua New Guinea'), - /* I18N: Name of a country or state */ - 'POL' => I18N::translate('Poland'), - /* I18N: Name of a country or state */ - 'PRI' => I18N::translate('Puerto Rico'), - /* I18N: Name of a country or state */ - 'PRK' => I18N::translate('North Korea'), - /* I18N: Name of a country or state */ - 'PRT' => I18N::translate('Portugal'), - /* I18N: Name of a country or state */ - 'PRY' => I18N::translate('Paraguay'), - /* I18N: Name of a country or state */ - 'PSE' => I18N::translate('Occupied Palestinian Territory'), - /* I18N: Name of a country or state */ - 'PYF' => I18N::translate('French Polynesia'), - /* I18N: Name of a country or state */ - 'QAT' => I18N::translate('Qatar'), - /* I18N: Name of a country or state */ - 'REU' => I18N::translate('Reunion'), - /* I18N: Name of a country or state */ - 'ROM' => I18N::translate('Romania'), - /* I18N: Name of a country or state */ - 'RUS' => I18N::translate('Russia'), - /* I18N: Name of a country or state */ - 'RWA' => I18N::translate('Rwanda'), - /* I18N: Name of a country or state */ - 'SAU' => I18N::translate('Saudi Arabia'), - /* I18N: Name of a country or state */ - 'SCT' => I18N::translate('Scotland'), - /* I18N: Name of a country or state */ - 'SDN' => I18N::translate('Sudan'), - /* I18N: Name of a country or state */ - 'SEA' => I18N::translate('At sea'), - /* I18N: Name of a country or state */ - 'SEN' => I18N::translate('Senegal'), - /* I18N: Name of a country or state */ - 'SER' => I18N::translate('Serbia'), - /* I18N: Name of a country or state */ - 'SGP' => I18N::translate('Singapore'), - /* I18N: Name of a country or state */ - 'SGS' => I18N::translate('South Georgia and the South Sandwich Islands'), - /* I18N: Name of a country or state */ - 'SHN' => I18N::translate('Saint Helena'), - /* I18N: Name of a country or state */ - 'SJM' => I18N::translate('Svalbard and Jan Mayen'), - /* I18N: Name of a country or state */ - 'SLB' => I18N::translate('Solomon Islands'), - /* I18N: Name of a country or state */ - 'SLE' => I18N::translate('Sierra Leone'), - /* I18N: Name of a country or state */ - 'SLV' => I18N::translate('El Salvador'), - /* I18N: Name of a country or state */ - 'SMR' => I18N::translate('San Marino'), - /* I18N: Name of a country or state */ - 'SOM' => I18N::translate('Somalia'), - /* I18N: Name of a country or state */ - 'SPM' => I18N::translate('Saint Pierre and Miquelon'), - /* I18N: Name of a country or state */ - 'SSD' => I18N::translate('South Sudan'), - /* I18N: Name of a country or state */ - 'STP' => I18N::translate('Sao Tome and Principe'), - /* I18N: Name of a country or state */ - 'SUR' => I18N::translate('Suriname'), - /* I18N: Name of a country or state */ - 'SVK' => I18N::translate('Slovakia'), - /* I18N: Name of a country or state */ - 'SVN' => I18N::translate('Slovenia'), - /* I18N: Name of a country or state */ - 'SWE' => I18N::translate('Sweden'), - /* I18N: Name of a country or state */ - 'SWZ' => I18N::translate('Swaziland'), - // SXM => Sint Maarten - /* I18N: Name of a country or state */ - 'SYC' => I18N::translate('Seychelles'), - /* I18N: Name of a country or state */ - 'SYR' => I18N::translate('Syria'), - /* I18N: Name of a country or state */ - 'TCA' => I18N::translate('Turks and Caicos Islands'), - /* I18N: Name of a country or state */ - 'TCD' => I18N::translate('Chad'), - /* I18N: Name of a country or state */ - 'TGO' => I18N::translate('Togo'), - /* I18N: Name of a country or state */ - 'THA' => I18N::translate('Thailand'), - /* I18N: Name of a country or state */ - 'TJK' => I18N::translate('Tajikistan'), - /* I18N: Name of a country or state */ - 'TKL' => I18N::translate('Tokelau'), - /* I18N: Name of a country or state */ - 'TKM' => I18N::translate('Turkmenistan'), - /* I18N: Name of a country or state */ - 'TLS' => I18N::translate('Timor-Leste'), - /* I18N: Name of a country or state */ - 'TON' => I18N::translate('Tonga'), - /* I18N: Name of a country or state */ - 'TTO' => I18N::translate('Trinidad and Tobago'), - /* I18N: Name of a country or state */ - 'TUN' => I18N::translate('Tunisia'), - /* I18N: Name of a country or state */ - 'TUR' => I18N::translate('Turkey'), - /* I18N: Name of a country or state */ - 'TUV' => I18N::translate('Tuvalu'), - /* I18N: Name of a country or state */ - 'TWN' => I18N::translate('Taiwan'), - /* I18N: Name of a country or state */ - 'TZA' => I18N::translate('Tanzania'), - /* I18N: Name of a country or state */ - 'UGA' => I18N::translate('Uganda'), - /* I18N: Name of a country or state */ - 'UKR' => I18N::translate('Ukraine'), - /* I18N: Name of a country or state */ - 'UMI' => I18N::translate('US Minor Outlying Islands'), - /* I18N: Name of a country or state */ - 'URY' => I18N::translate('Uruguay'), - /* I18N: Name of a country or state */ - 'USA' => I18N::translate('United States'), - /* I18N: Name of a country or state */ - 'UZB' => I18N::translate('Uzbekistan'), - /* I18N: Name of a country or state */ - 'VAT' => I18N::translate('Vatican City'), - /* I18N: Name of a country or state */ - 'VCT' => I18N::translate('Saint Vincent and the Grenadines'), - /* I18N: Name of a country or state */ - 'VEN' => I18N::translate('Venezuela'), - /* I18N: Name of a country or state */ - 'VGB' => I18N::translate('British Virgin Islands'), - /* I18N: Name of a country or state */ - 'VIR' => I18N::translate('US Virgin Islands'), - /* I18N: Name of a country or state */ - 'VNM' => I18N::translate('Vietnam'), - /* I18N: Name of a country or state */ - 'VUT' => I18N::translate('Vanuatu'), - /* I18N: Name of a country or state */ - 'WLF' => I18N::translate('Wallis and Futuna'), - /* I18N: Name of a country or state */ - 'WLS' => I18N::translate('Wales'), - /* I18N: Name of a country or state */ - 'WSM' => I18N::translate('Samoa'), - /* I18N: Name of a country or state */ - 'YEM' => I18N::translate('Yemen'), - /* I18N: Name of a country or state */ - 'ZAF' => I18N::translate('South Africa'), - /* I18N: Name of a country or state */ - 'ZMB' => I18N::translate('Zambia'), - /* I18N: Name of a country or state */ - 'ZWE' => I18N::translate('Zimbabwe'), - ]; - } - - /** - * Century name, English => 21st, Polish => XXI, etc. - * - * @param int $century + * What is the current version of webtrees. * * @return string */ - private function centuryName($century): string + public function webtreesVersion(): string { - if ($century < 0) { - return I18N::translate('%s BCE', $this->centuryName(-$century)); - } - - // The current chart engine (Google charts) can't handle <sup></sup> 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'; - } + return Webtrees::VERSION; } } diff --git a/resources/views/modules/statistics-chart/families.phtml b/resources/views/modules/statistics-chart/families.phtml index 9097fa3bd7..dbbab97f21 100644 --- a/resources/views/modules/statistics-chart/families.phtml +++ b/resources/views/modules/statistics-chart/families.phtml @@ -1,144 +1,12 @@ -<?php use Fisharebest\Webtrees\I18N; ?> +<?php +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> -<h3> - <?= I18N::translate('Total families: %s', $stats->totalFamilies()) ?> -</h3> - -<table> - <tr> - <td><?= I18N::translate('Total marriages') ?></td> - <td><?= I18N::translate('Total divorces') ?></td> - </tr> - <tr> - <td><?= $stats->totalMarriages() ?></td> - <td><?= $stats->totalDivorces() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Marriages by century') ?></td> - <td><?= I18N::translate('Divorces by century') ?></td> - </tr> - <tr> - <td><?= $stats->statsMarr() ?></td> - <td><?= $stats->statsDiv() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Earliest marriage') ?></td> - <td><?= I18N::translate('Earliest divorce') ?></td> - </tr> - <tr> - <td><?= $stats->firstMarriage() ?></td> - <td><?= $stats->firstDivorce() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Latest marriage') ?></td> - <td><?= I18N::translate('Latest divorce') ?></td> - </tr> - <tr> - <td><?= $stats->lastMarriage() ?></td> - <td><?= $stats->lastDivorce() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Length of marriage') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Longest marriage'), ' - ', $stats->topAgeOfMarriage() ?></td> - <td><?= I18N::translate('Shortest marriage'), ' - ', $stats->minAgeOfMarriage() ?></td> - </tr> - <tr> - <td><?= $stats->topAgeOfMarriageFamily() ?></td> - <td><?= $stats->minAgeOfMarriageFamily() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Age in year of marriage') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Youngest male'), ' - ', $stats->youngestMarriageMaleAge(true) ?></td> - <td><?= I18N::translate('Youngest female'), ' - ', $stats->youngestMarriageFemaleAge(true) ?></td> - </tr> - <tr> - <td><?= $stats->youngestMarriageMale() ?></td> - <td><?= $stats->youngestMarriageFemale() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Oldest male'), ' - ', $stats->oldestMarriageMaleAge(true) ?></td> - <td><?= I18N::translate('Oldest female'), ' - ', $stats->oldestMarriageFemaleAge(true) ?></td> - </tr> - <tr> - <td><?= $stats->oldestMarriageMale() ?></td> - <td><?= $stats->oldestMarriageFemale() ?></td> - </tr> - <tr> - <td colspan="2"><?= $stats->statsMarrAge() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Age at birth of child') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Youngest father'), ' - ', $stats->youngestFatherAge(true) ?></td> - <td><?= I18N::translate('Youngest mother'), ' - ', $stats->youngestMotherAge(true) ?></td> - </tr> - <tr> - <td><?= $stats->youngestFather() ?></td> - <td><?= $stats->youngestMother() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Oldest father'), ' - ', $stats->oldestFatherAge(true) ?></td> - <td><?= I18N::translate('Oldest mother'), ' - ', $stats->oldestMotherAge(true) ?></td> - </tr> - <tr> - <td><?= $stats->oldestFather() ?></td> - <td><?= $stats->oldestMother() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Children in family') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Average number of children per family') ?></td> - <td><?= I18N::translate('Number of families without children') ?></td> - </tr> - <tr> - <td><?= $stats->averageChildren() ?></td> - <td><?= $stats->noChildrenFamilies() ?></td> - </tr> - <tr> - <td><?= $stats->statsChildren() ?></td> - <td><?= $stats->chartNoChildrenFamilies() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Largest families') ?></td> - <td><?= I18N::translate('Largest number of grandchildren') ?></td> - </tr> - <tr> - <td><?= $stats->topTenLargestFamilyList() ?></td> - <td><?= $stats->topTenLargestGrandFamilyList() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Age difference') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Age between siblings') ?></td> - <td><?= I18N::translate('Greatest age between siblings') ?></td> - </tr> - <tr> - <td><?= $stats->topAgeBetweenSiblingsList() ?></td> - <td><?= $stats->topAgeBetweenSiblingsFullName() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Age between husband and wife') ?></td> - <td><?= I18N::translate('Age between wife and husband') ?></td> - </tr> - <tr> - <td><?= $stats->ageBetweenSpousesMFList() ?></td> - <td><?= $stats->ageBetweenSpousesFMList() ?></td> - </tr> -</table> +<div class="container pt-3"> + <?= view('statistics/families/total-records', ['stats' => $stats]) ?> + <?= view('statistics/families/marriage-length', ['stats' => $stats]) ?> + <?= view('statistics/families/marriage-age', ['stats' => $stats]) ?> + <?= view('statistics/families/birth-age', ['stats' => $stats]) ?> + <?= view('statistics/families/children', ['stats' => $stats]) ?> + <?= view('statistics/families/age-difference', ['stats' => $stats]) ?> +</div> diff --git a/resources/views/modules/statistics-chart/individuals.phtml b/resources/views/modules/statistics-chart/individuals.phtml index 7980441272..9fadb3c699 100644 --- a/resources/views/modules/statistics-chart/individuals.phtml +++ b/resources/views/modules/statistics-chart/individuals.phtml @@ -1,136 +1,14 @@ <?php use Fisharebest\Webtrees\I18N; ?> -<h3> - <?= I18N::translate('Total individuals: %s', $stats->totalIndividuals()) ?> -</h3> +<?php +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> -<table class="table table-sm table-bordered"> - <thead> - <tr> - <th><?= I18N::translate('Total males') ?></th> - <th><?= I18N::translate('Total females') ?></th> - <th><?= I18N::translate('Total living') ?></th> - <th><?= I18N::translate('Total dead') ?></th> - </tr> - </thead> - <tbody> - <tr> - <td><?= $stats->totalSexMales() ?></td> - <td><?= $stats->totalSexFemales() ?></td> - <td><?= $stats->totalLiving() ?></td> - <td><?= $stats->totalDeceased() ?></td> - </tr> - </tbody> - <tfoot> - <tr> - <td colspan="2"><?= $stats->chartSex() ?></td> - <td colspan="2"><?= $stats->chartMortality() ?></td> - </tr> - </tfoot> -</table> - -<h3><?= I18N::translate('Events') ?></h3> - -<table class="table table-sm table-bordered"> - <tbody> - <tr> - <th><?= I18N::translate('Total births') ?></th> - <th><?= I18N::translate('Total deaths') ?></th> - </tr> - <tr> - <td><?= $stats->totalBirths() ?></td> - <td><?= $stats->totalDeaths() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Births by century') ?></td> - <td><?= I18N::translate('Deaths by century') ?></td> - </tr> - <tr> - <td><?= $stats->statsBirth() ?></td> - <td><?= $stats->statsDeath() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Earliest birth') ?></td> - <td><?= I18N::translate('Earliest death') ?></td> - </tr> - <tr> - <td><?= $stats->firstBirth() ?></td> - <td><?= $stats->firstDeath() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Latest birth') ?></td> - <td><?= I18N::translate('Latest death') ?></td> - </tr> - <tr> - <td><?= $stats->lastBirth() ?></td> - <td><?= $stats->lastDeath() ?></td> - </tr> - </tbody> -</table> - -<h3><?= I18N::translate('Lifespan') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Average age at death') ?></td> - <td><?= I18N::translate('Males') ?></td> - <td><?= I18N::translate('Females') ?></td> - </tr> - <tr> - <td><?= $stats->averageLifespan(true) ?></td> - <td><?= $stats->averageLifespanMale(true) ?></td> - <td><?= $stats->averageLifespanFemale(true) ?></td> - </tr> - <tr> - <td colspan="3"><?= $stats->statsAge() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Greatest age at death') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Males') ?></td> - <td><?= I18N::translate('Females') ?></td> - </tr> - <tr> - <td><?= $stats->topTenOldestMaleList() ?></td> - <td><?= $stats->topTenOldestFemaleList() ?></td> - </tr> -</table> - -<?php if ($show_oldest_living) : ?> - <h3><?= I18N::translate('Oldest living individuals') ?></h3> - - <table> - <tr> - <td><?= I18N::translate('Males') ?></td> - <td><?= I18N::translate('Females') ?></td> - </tr> - <tr> - <td><?= $stats->topTenOldestMaleListAlive() ?></td> - <td><?= $stats->topTenOldestFemaleListAlive() ?></td> - </tr> - </table> -<?php endif ?> - -<h3><?= I18N::translate('Names') ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Total surnames') ?></td> - <td><?= I18N::translate('Total given names') ?></td> - </tr> - <tr> - <td><?= $stats->totalSurnames() ?></td> - <td><?= $stats->totalGivennames() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Top surnames') ?></td> - <td><?= I18N::translate('Top given names') ?></td> - </tr> - <tr> - <td><?= $stats->chartCommonSurnames() ?></td> - <td><?= $stats->chartCommonGiven() ?></td> - </tr> -</table> +<div class="container pt-3"> + <?= view('statistics/individuals/total-records', ['stats' => $stats]) ?> + <?= view('statistics/individuals/total-events', ['stats' => $stats]) ?> + <?= view('statistics/individuals/lifespan', ['stats' => $stats]) ?> + <?= view('statistics/individuals/greatest-age', ['stats' => $stats]) ?> + <?= view('statistics/individuals/oldest-living', ['stats' => $stats]) ?> + <?= view('statistics/individuals/names', ['stats' => $stats]) ?> +</div> diff --git a/resources/views/modules/statistics-chart/other.phtml b/resources/views/modules/statistics-chart/other.phtml index d30979de96..d6e374e4a0 100644 --- a/resources/views/modules/statistics-chart/other.phtml +++ b/resources/views/modules/statistics-chart/other.phtml @@ -1,85 +1,12 @@ -<?php use Fisharebest\Webtrees\I18N; ?> +<?php +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> -<h3> - <?= I18N::translate('Records') ?>: <?= $stats->totalRecords() ?> -</h3> - -<table> - <tr> - <td><?= I18N::translate('Media objects') ?></td> - <td><?= I18N::translate('Sources') ?></td> - <td><?= I18N::translate('Notes') ?></td> - <td><?= I18N::translate('Repositories') ?></td> - </tr> - <tr> - <td><?= $stats->totalMedia() ?></td> - <td><?= $stats->totalSources() ?></td> - <td><?= $stats->totalNotes() ?></td> - <td><?= $stats->totalRepositories() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Total events'), ': ', $stats->totalEvents() ?></h3> - -<table> - <tr> - <td><?= I18N::translate('First event'), ' - ', $stats->firstEventType() ?></td> - <td><?= I18N::translate('Last event'), ' - ', $stats->lastEventType() ?></td> - </tr> - <tr> - <td><?= $stats->firstEvent() ?></td> - <td><?= $stats->lastEvent() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Media objects'), ': ', $stats->totalMedia() ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Media objects') ?></td> - </tr> - <tr> - <td><?= $stats->chartMedia() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Sources'), ': ', $stats->totalSources() ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Individuals with sources') ?></td> - <td><?= I18N::translate('Families with sources') ?></td> - </tr> - <tr> - <td><?= $stats->totalIndisWithSources() ?></td> - <td><?= $stats->totalFamsWithSources() ?></td> - </tr> - <tr> - <td><?= $stats->chartIndisWithSources() ?></td> - <td><?= $stats->chartFamsWithSources() ?></td> - </tr> -</table> - -<h3><?= I18N::translate('Places'), ': ', $stats->totalPlaces() ?></h3> - -<table> - <tr> - <td><?= I18N::translate('Birth places') ?></td> - <td><?= I18N::translate('Death places') ?></td> - </tr> - <tr> - <td><?= $stats->commonBirthPlacesList() ?></td> - <td><?= $stats->commonDeathPlacesList() ?></td> - </tr> - <tr> - <td><?= I18N::translate('Marriage places') ?></td> - <td><?= I18N::translate('Events in countries') ?></td> - </tr> - <tr> - <td><?= $stats->commonMarriagePlacesList() ?></td> - <td><?= $stats->commonCountriesList() ?></td> - </tr> - <tr> - <td colspan="2"><?= $stats->chartDistribution() ?></td> - </tr> -</table> +<div class="container pt-3"> + <?= view('statistics/other/total-records', ['stats' => $stats]) ?> + <?= view('statistics/other/total-events', ['stats' => $stats]) ?> + <?= view('statistics/other/chart-objects', ['stats' => $stats]) ?> + <?= view('statistics/other/chart-sources', ['stats' => $stats]) ?> + <?= view('statistics/other/places', ['stats' => $stats]) ?> + <?= $stats->chartDistribution() ?> +</div> diff --git a/resources/views/statistics/families/age-difference.phtml b/resources/views/statistics/families/age-difference.phtml new file mode 100644 index 0000000000..246770cb3e --- /dev/null +++ b/resources/views/statistics/families/age-difference.phtml @@ -0,0 +1,55 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Age difference') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Age between siblings') ?> + </h5> + <?= $stats->topAgeBetweenSiblingsList() ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Greatest age between siblings') ?> + </h5> + <div class="card-body"> + <?= $stats->topAgeBetweenSiblingsFullName() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Age between husband and wife') ?> + </h5> + <?= $stats->ageBetweenSpousesMFList() ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Age between wife and husband') ?> + </h5> + <?= $stats->ageBetweenSpousesFMList() ?> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/families/birth-age.phtml b/resources/views/statistics/families/birth-age.phtml new file mode 100644 index 0000000000..4635120b3f --- /dev/null +++ b/resources/views/statistics/families/birth-age.phtml @@ -0,0 +1,61 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Age at birth of child') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Youngest father'), ' - ', $stats->youngestFatherAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->youngestFather() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Youngest mother'), ' - ', $stats->youngestMotherAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->youngestMother() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Oldest father'), ' - ', $stats->oldestFatherAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->oldestFather() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Oldest mother'), ' - ', $stats->oldestMotherAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->oldestMother() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/families/children.phtml b/resources/views/statistics/families/children.phtml new file mode 100644 index 0000000000..f45f62405e --- /dev/null +++ b/resources/views/statistics/families/children.phtml @@ -0,0 +1,59 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Children in family') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Average number of children per family') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->averageChildren() ?></span> + </h5> + <div class="card-body text-center"> + <?= $stats->statsChildren() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Number of families without children') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->noChildrenFamilies() ?></span> + </h5> + <div class="card-body text-center"> + <?= $stats->chartNoChildrenFamilies() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Largest families') ?> + </h5> + <?= $stats->topTenLargestFamilyList(); ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Largest number of grandchildren') ?> + </h5> + <?= $stats->topTenLargestGrandFamilyList(); ?> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/families/marriage-age.phtml b/resources/views/statistics/families/marriage-age.phtml new file mode 100644 index 0000000000..fa8d52dbee --- /dev/null +++ b/resources/views/statistics/families/marriage-age.phtml @@ -0,0 +1,71 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Age in year of marriage') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Youngest male'), ' - ', $stats->youngestMarriageMaleAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->youngestMarriageMale() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Youngest female'), ' - ', $stats->youngestMarriageFemaleAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->youngestMarriageFemale() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Oldest male'), ' - ', $stats->oldestMarriageMaleAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->oldestMarriageMale() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Oldest female'), ' - ', $stats->oldestMarriageFemaleAge('1') ?> + </h5> + <div class="card-body"> + <?= $stats->oldestMarriageFemale() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="col-lg-12 col-md-12 mb-3"> + <div class="card m-0"> + <div class="card-body text-center"> + <?= $stats->statsMarrAge() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/families/marriage-length.phtml b/resources/views/statistics/families/marriage-length.phtml new file mode 100644 index 0000000000..82813995d3 --- /dev/null +++ b/resources/views/statistics/families/marriage-length.phtml @@ -0,0 +1,37 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Length of marriage') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Longest marriage'), ' - ', $stats->topAgeOfMarriage() ?> + </h5> + <div class="card-body"> + <?= $stats->topAgeOfMarriageFamily() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Shortest marriage'), ' - ', $stats->minAgeOfMarriage() ?> + </h5> + <div class="card-body"> + <?= $stats->minAgeOfMarriageFamily() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/families/top10-list-age.phtml b/resources/views/statistics/families/top10-list-age.phtml new file mode 100644 index 0000000000..9728c2422f --- /dev/null +++ b/resources/views/statistics/families/top10-list-age.phtml @@ -0,0 +1,28 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Individual; + +/* @var array $records */ +/* @var Family $family */ +/* @var Individual $child1 */ +/* @var Individual $child2 */ +?> + +<ul class="list-group list-group-flush"> + <?php foreach ($records as $record): ?> + <?php $child1 = $record['child1']; ?> + <?php $child2 = $record['child2']; ?> + <?php $family = $record['family']; ?> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <span> + <a href="<?= e($child2->url()) ?>"><?= $child2->getFullName() ?></a> <?= I18N::translate('and') ?> <a href="<?= e($child1->url()) ?>"><?= $child1->getFullName() ?></a> + <br> + <a href="<?= e($family->url()) ?>">[<?= I18N::translate('View this family') ?>]</a> + </span> + <span class="badge badge-secondary badge-pill ml-3"><?= $record['age'] ?></span> + </li> + <?php endforeach; ?> +</ul> diff --git a/resources/views/statistics/families/top10-list-grand.phtml b/resources/views/statistics/families/top10-list-grand.phtml new file mode 100644 index 0000000000..bb2c1c2f64 --- /dev/null +++ b/resources/views/statistics/families/top10-list-grand.phtml @@ -0,0 +1,21 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; + +/* @var array $records */ +/* @var Family $family */ +?> + +<ul class="list-group list-group-flush"> + <?php foreach ($records as $record): ?> + <?php $family = $record['family']; ?> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <a href="<?= e($family->url()) ?>"><?= $family->getFullName() ?></a> + <span class="badge badge-secondary badge-pill ml-3"> + <?= I18N::plural('%s grandchild', '%s grandchildren', $record['count'], I18N::number($record['count'])) ?> + </span> + </li> + <?php endforeach; ?> +</ul> diff --git a/resources/views/statistics/families/top10-list-spouses.phtml b/resources/views/statistics/families/top10-list-spouses.phtml new file mode 100644 index 0000000000..3575f0eb8a --- /dev/null +++ b/resources/views/statistics/families/top10-list-spouses.phtml @@ -0,0 +1,18 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; + +/* @var array $records */ +/* @var Family $family */ +?> + +<ul class="list-group list-group-flush"> + <?php foreach ($records as $record): ?> + <?php $family = $record['family']; ?> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <a href="<?= e($family->url()) ?>"><?= $family->getFullName() ?></a> + <span class="badge badge-secondary badge-pill ml-3"><?= $record['age'] ?></span> + </li> + <?php endforeach; ?> +</ul> diff --git a/resources/views/statistics/families/top10-list.phtml b/resources/views/statistics/families/top10-list.phtml new file mode 100644 index 0000000000..18e1fd470a --- /dev/null +++ b/resources/views/statistics/families/top10-list.phtml @@ -0,0 +1,21 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; + +/* @var array $records */ +/* @var Family $family */ +?> + +<ul class="list-group list-group-flush"> + <?php foreach ($records as $record): ?> + <?php $family = $record['family']; ?> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <a href="<?= e($family->url()) ?>"><?= $family->getFullName() ?></a> + <span class="badge badge-secondary badge-pill ml-3"> + <?= I18N::plural('%s child', '%s children', $record['count'], I18N::number($record['count'])) ?> + </span> + </li> + <?php endforeach; ?> +</ul> diff --git a/resources/views/statistics/families/top10-nolist-age.phtml b/resources/views/statistics/families/top10-nolist-age.phtml new file mode 100644 index 0000000000..13e008cad3 --- /dev/null +++ b/resources/views/statistics/families/top10-nolist-age.phtml @@ -0,0 +1,24 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Individual; + +/* @var array $record */ +/* @var Family $family */ +/* @var Individual $child1 */ +/* @var Individual $child2 */ +?> + +<?php $child1 = $record['child1']; ?> +<?php $child2 = $record['child2']; ?> +<?php $family = $record['family']; ?> + +<?= $child2->formatList() ?> +<br> +<?= I18N::translate('and') ?> +<br> +<?= $child1->formatList() ?> +<br> +<a href="<?= e($family->url()) ?>">[<?= I18N::translate('View this family') ?>]</a> diff --git a/resources/views/statistics/families/top10-nolist-grand.phtml b/resources/views/statistics/families/top10-nolist-grand.phtml new file mode 100644 index 0000000000..6c58c6b868 --- /dev/null +++ b/resources/views/statistics/families/top10-nolist-grand.phtml @@ -0,0 +1,15 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; + +/* @var array $records */ +/* @var Family $family */ +?> + +<?php foreach ($records as $record): ?> + <?php $family = $record['family']; ?> + <a href="<?= e($family->url()) ?>"><?= $family->getFullName() ?></a> + - <?= I18N::plural('%s grandchild', '%s grandchildren', $record['count'], I18N::number($record['count'])) ?> +<?php endforeach; ?> diff --git a/resources/views/statistics/families/top10-nolist-spouses.phtml b/resources/views/statistics/families/top10-nolist-spouses.phtml new file mode 100644 index 0000000000..50a09aac07 --- /dev/null +++ b/resources/views/statistics/families/top10-nolist-spouses.phtml @@ -0,0 +1,13 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; + +/* @var array $records */ +/* @var Family $family */ +?> + +<?php foreach ($records as $record): ?> + <?php $family = $record['family']; ?> + <a href="<?= e($family->url()) ?>"><?= $family->getFullName() ?></a> (<?= $record['age'] ?>) +<?php endforeach; ?> diff --git a/resources/views/statistics/families/top10-nolist.phtml b/resources/views/statistics/families/top10-nolist.phtml new file mode 100644 index 0000000000..a60d371fec --- /dev/null +++ b/resources/views/statistics/families/top10-nolist.phtml @@ -0,0 +1,15 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Family; +use Fisharebest\Webtrees\I18N; + +/* @var array $records */ +/* @var Family $family */ +?> + +<?php foreach ($records as $record): ?> + <?php $family = $record['family']; ?> + <a href="<?= e($family->url()) ?>"><?= $family->getFullName() ?></a> + - <?= I18N::plural('%s child', '%s children', $record['count'], I18N::number($record['count'])) ?> +<?php endforeach; ?> diff --git a/resources/views/statistics/families/total-records.phtml b/resources/views/statistics/families/total-records.phtml new file mode 100644 index 0000000000..e9b67e2806 --- /dev/null +++ b/resources/views/statistics/families/total-records.phtml @@ -0,0 +1,112 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Total families') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalFamilies() ?></span> +</h4> + +<div class="mb-3"> + <div class="row"> + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total marriages') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalMarriages() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Marriages by century') ?> + </h5> + <div class="card-body"> + <?= $stats->statsMarr() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Earliest marriage') ?> + </h5> + <div class="card-body"> + <?= $stats->firstMarriage() ?> + </div> + </div> + </div> + + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Latest marriage') ?> + </h5> + <div class="card-body"> + <?= $stats->lastMarriage() ?> + </div> + </div> + </div> + </div> + </div> + + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total divorces') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalDivorces() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Divorces by century') ?> + </h5> + <div class="card-body"> + <?= $stats->statsDiv() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Earliest divorce') ?> + </h5> + <div class="card-body"> + <?= $stats->firstDivorce() ?> + </div> + </div> + </div> + + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Latest divorce') ?> + </h5> + <div class="card-body"> + <?= $stats->lastDivorce() ?> + </div> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/hit-count.phtml b/resources/views/statistics/hit-count.phtml new file mode 100644 index 0000000000..cc015b6b7e --- /dev/null +++ b/resources/views/statistics/hit-count.phtml @@ -0,0 +1,8 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; +?> +<span class="odometer"> + <?= I18N::digits($count); ?> +</span> diff --git a/resources/views/statistics/individuals/greatest-age.phtml b/resources/views/statistics/individuals/greatest-age.phtml new file mode 100644 index 0000000000..caaa6b0014 --- /dev/null +++ b/resources/views/statistics/individuals/greatest-age.phtml @@ -0,0 +1,33 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Greatest age at death') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Males') ?> + </h5> + <?= $stats->topTenOldestMaleList() ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Females') ?> + </h5> + <?= $stats->topTenOldestFemaleList() ?> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/individuals/lifespan.phtml b/resources/views/statistics/individuals/lifespan.phtml new file mode 100644 index 0000000000..637ce7c2df --- /dev/null +++ b/resources/views/statistics/individuals/lifespan.phtml @@ -0,0 +1,52 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Lifespan') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Average age at death') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->averageLifespan(true) ?></span> + </h5> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Males') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->averageLifespanMale(true) ?></span> + </h5> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Females') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->averageLifespanFemale(true) ?></span> + </h5> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="col-12 mb-3"> + <div class="card m-0"> + <div class="card-body text-center"> + <?= $stats->statsAge() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/individuals/names.phtml b/resources/views/statistics/individuals/names.phtml new file mode 100644 index 0000000000..bc00056b9a --- /dev/null +++ b/resources/views/statistics/individuals/names.phtml @@ -0,0 +1,63 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Names') ?> +</h4> + +<div class="mb-3"> + <div class="row"> + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total surnames') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalSurnames() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Top surnames') ?> + </h5> + <div class="card-body"> + <?= $stats->chartCommonSurnames() ?> + </div> + </div> + </div> + </div> + </div> + + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total given names') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalGivennames() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Top given names') ?> + </h5> + <div class="card-body"> + <?= $stats->chartCommonGiven() ?> + </div> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/individuals/oldest-living.phtml b/resources/views/statistics/individuals/oldest-living.phtml new file mode 100644 index 0000000000..288f6f934c --- /dev/null +++ b/resources/views/statistics/individuals/oldest-living.phtml @@ -0,0 +1,33 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Oldest living individuals') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Males') ?> + </h5> + <?= $stats->topTenOldestMaleListAlive() ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Females') ?> + </h5> + <?= $stats->topTenOldestFemaleListAlive() ?> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/individuals/top10-list.phtml b/resources/views/statistics/individuals/top10-list.phtml new file mode 100644 index 0000000000..c1a1d32330 --- /dev/null +++ b/resources/views/statistics/individuals/top10-list.phtml @@ -0,0 +1,18 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Individual; + +/* @var array $records */ +/* @var Individual $person */ +?> + +<ul class="list-group list-group-flush"> + <?php foreach ($records as $record): ?> + <?php $person = $record['person']; ?> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <a href="<?= e($person->url()) ?>"><?= $person->getFullName() ?></a> + <span class="badge badge-secondary badge-pill ml-3"><?= $record['age'] ?></span> + </li> + <?php endforeach; ?> +</ul> diff --git a/resources/views/statistics/individuals/top10-nolist.phtml b/resources/views/statistics/individuals/top10-nolist.phtml new file mode 100644 index 0000000000..e3e52d9a91 --- /dev/null +++ b/resources/views/statistics/individuals/top10-nolist.phtml @@ -0,0 +1,13 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\Individual; + +/* @var array $records */ +/* @var Individual $person */ +?> + +<?php foreach ($records as $record): ?> + <?php $person = $record['person']; ?> + <a href="<?= e($person->url()) ?>"><?= $person->getFullName() ?></a> (<?= $record['age'] ?>) +<?php endforeach; ?> diff --git a/resources/views/statistics/individuals/total-events.phtml b/resources/views/statistics/individuals/total-events.phtml new file mode 100644 index 0000000000..c3e27cd0e8 --- /dev/null +++ b/resources/views/statistics/individuals/total-events.phtml @@ -0,0 +1,111 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Events') ?> +</h4> + +<div class="mb-3"> + <div class="row"> + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total births') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalBirths() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Births by century') ?> + </h5> + <div class="card-body"> + <?= $stats->statsBirth() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Earliest birth') ?> + </h5> + <div class="card-body"> + <?= $stats->firstBirth() ?> + </div> + </div> + </div> + + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Latest birth') ?> + </h5> + <div class="card-body"> + <?= $stats->lastBirth() ?> + </div> + </div> + </div> + </div> + </div> + + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total deaths') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalDeaths() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Deaths by century') ?> + </h5> + <div class="card-body"> + <?= $stats->statsDeath() ?> + </div> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Earliest death') ?> + </h5> + <div class="card-body"> + <?= $stats->firstDeath() ?> + </div> + </div> + </div> + + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Latest death') ?> + </h5> + <div class="card-body"> + <?= $stats->lastDeath() ?> + </div> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/individuals/total-records.phtml b/resources/views/statistics/individuals/total-records.phtml new file mode 100644 index 0000000000..a28777ca23 --- /dev/null +++ b/resources/views/statistics/individuals/total-records.phtml @@ -0,0 +1,77 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Total individuals') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalIndividuals() ?></span> +</h4> + +<div class="mb-3"> + <div class="row"> + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total males') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalSexMales() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total females') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalSexFemales() ?></span> + </h5> + </div> + </div> + + <div class="mb-3 col-12"> + <div class="card m-0"> + <div class="card-body"> + <?= $stats->chartSex() ?> + </div> + </div> + </div> + </div> + </div> + + <div class="col-12 col-lg-6"> + <div class="card-deck"> + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total living') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalLiving() ?></span> + </h5> + </div> + </div> + <div class="mb-3 col-lg-12 col-md-6"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Total dead') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalDeceased() ?></span> + </h5> + </div> + </div> + </div> + + <div class="card-deck"> + <div class="mb-3 col-12"> + <div class="card m-0"> + <div class="card-body"> + <?= $stats->chartMortality() ?> + </div> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/other/chart-distribution.phtml b/resources/views/statistics/other/chart-distribution.phtml new file mode 100644 index 0000000000..fe75a0e30f --- /dev/null +++ b/resources/views/statistics/other/chart-distribution.phtml @@ -0,0 +1,31 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= $chart_title ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 mb-3"> + <div class="card m-0"> + <div class="card-body"> + <div id="google_charts" class="text-center"> + <img src="<?= $chart_url ?>" alt="<?= $chart_title ?>" title="<?= $chart_title ?>" class="gchart" /> + <br> + <table class="center"> + <tr> + <td bgcolor="#<?= $chart_color2 ?>" width="12"></td><td><?= I18N::translate('Highest population') ?></td> + <td bgcolor="#<?= $chart_color3 ?>" width="12"></td><td><?= I18N::translate('Lowest population') ?></td> + <td bgcolor="#<?= $chart_color1 ?>" width="12"></td><td><?= I18N::translate('Nobody at all') ?></td> + </tr> + </table> + </div> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/other/chart-google.phtml b/resources/views/statistics/other/chart-google.phtml new file mode 100644 index 0000000000..5172f56d28 --- /dev/null +++ b/resources/views/statistics/other/chart-google.phtml @@ -0,0 +1 @@ +<img src="<?= $chart_url ?>" width="<?= $sizes[0] ?>" height="<?= $sizes[1] ?>" alt="<?= $chart_title ?>" title="<?= $chart_title ?>" /> diff --git a/resources/views/statistics/other/chart-objects.phtml b/resources/views/statistics/other/chart-objects.phtml new file mode 100644 index 0000000000..df65296172 --- /dev/null +++ b/resources/views/statistics/other/chart-objects.phtml @@ -0,0 +1,23 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Media objects') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <div class="card-body"> + <?= $stats->chartMedia() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/other/chart-sources.phtml b/resources/views/statistics/other/chart-sources.phtml new file mode 100644 index 0000000000..720c1a119c --- /dev/null +++ b/resources/views/statistics/other/chart-sources.phtml @@ -0,0 +1,39 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Sources') ?> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-lg-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Individuals with sources') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalIndisWithSources() ?></span> + </h5> + <div class="card-body"> + <?= $stats->chartIndisWithSources() ?> + </div> + </div> + </div> + + <div class="col-12 col-lg-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Families with sources') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalFamsWithSources() ?></span> + </h5> + <div class="card-body"> + <?= $stats->chartFamsWithSources() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/other/places.phtml b/resources/views/statistics/other/places.phtml new file mode 100644 index 0000000000..ef4a037fcb --- /dev/null +++ b/resources/views/statistics/other/places.phtml @@ -0,0 +1,53 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Place; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Places') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalPlaces() ?></span> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Birth places') ?> + </h5> + <?= $stats->commonBirthPlacesList(); ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Death places') ?> + </h5> + <?= $stats->commonDeathPlacesList(); ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Marriage places') ?> + </h5> + <?= $stats->commonMarriagePlacesList(); ?> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Events in countries') ?> + </h5> + <?= $stats->commonCountriesList(); ?> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/other/top10-list.phtml b/resources/views/statistics/other/top10-list.phtml new file mode 100644 index 0000000000..27d3638f1c --- /dev/null +++ b/resources/views/statistics/other/top10-list.phtml @@ -0,0 +1,19 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; +use Fisharebest\Webtrees\Place; + +/* @var array $records */ +/* @var Place $place */ +?> + +<ul class="list-group list-group-flush"> + <?php foreach ($records as $record): ?> + <?php $place = $record['place']; ?> + <li class="list-group-item d-flex justify-content-between align-items-center"> + <a href="<?= e($place->url()) ?>"><?= $place->fullName() ?></a> + <span class="badge badge-secondary badge-pill ml-3"><?= I18N::number($record['count']) ?></span> + </li> + <?php endforeach; ?> +</ul> diff --git a/resources/views/statistics/other/total-events.phtml b/resources/views/statistics/other/total-events.phtml new file mode 100644 index 0000000000..776f1857f7 --- /dev/null +++ b/resources/views/statistics/other/total-events.phtml @@ -0,0 +1,38 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Total events') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalEvents() ?></span> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('First event'), ' - ', $stats->firstEventType() ?> + </h5> + <div class="card-body"> + <?= $stats->firstEvent() ?> + </div> + </div> + </div> + + <div class="col-12 col-md-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header"> + <?= I18N::translate('Last event'), ' - ', $stats->lastEventType() ?> + </h5> + <div class="card-body"> + <?= $stats->lastEvent() ?> + </div> + </div> + </div> + </div> +</div> diff --git a/resources/views/statistics/other/total-records.phtml b/resources/views/statistics/other/total-records.phtml new file mode 100644 index 0000000000..4c90246412 --- /dev/null +++ b/resources/views/statistics/other/total-records.phtml @@ -0,0 +1,52 @@ +<?php +declare(strict_types=1); + +use Fisharebest\Webtrees\I18N; + +/** @var \Fisharebest\Webtrees\Stats $stats */ +?> + +<h4 class="border-bottom p-2 mb-4"> + <?= I18N::translate('Records') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalRecords() ?></span> +</h4> + +<div class="mb-3"> + <div class="card-deck"> + <div class="col-12 col-lg-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Media objects') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalMedia() ?></span> + </h5> + </div> + </div> + + <div class="col-12 col-lg-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Sources') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalSources() ?></span> + </h5> + </div> + </div> + + <div class="col-12 col-lg-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Notes') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalNotes() ?></span> + </h5> + </div> + </div> + + <div class="col-12 col-lg-6 mb-3"> + <div class="card m-0"> + <h5 class="card-header border-bottom-0"> + <?= I18N::translate('Repositories') ?> + <span class="badge badge-secondary badge-pill float-right"><?= $stats->totalRepositories() ?></span> + </h5> + </div> + </div> + </div> +</div> |
