summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Module/StatisticsChartModule.php46
-rw-r--r--app/Statistics/AbstractGoogle.php91
-rw-r--r--app/Statistics/Google/ChartAge.php170
-rw-r--r--app/Statistics/Google/ChartBirth.php126
-rw-r--r--app/Statistics/Google/ChartChildren.php144
-rw-r--r--app/Statistics/Google/ChartCommonGiven.php96
-rw-r--r--app/Statistics/Google/ChartCommonSurname.php142
-rw-r--r--app/Statistics/Google/ChartDeath.php127
-rw-r--r--app/Statistics/Google/ChartDistribution.php236
-rw-r--r--app/Statistics/Google/ChartDivorce.php126
-rw-r--r--app/Statistics/Google/ChartFamilyLargest.php131
-rw-r--r--app/Statistics/Google/ChartFamilyWithSources.php77
-rw-r--r--app/Statistics/Google/ChartIndividual.php73
-rw-r--r--app/Statistics/Google/ChartMarriage.php127
-rw-r--r--app/Statistics/Google/ChartMarriageAge.php217
-rw-r--r--app/Statistics/Google/ChartMedia.php88
-rw-r--r--app/Statistics/Google/ChartMortality.php107
-rw-r--r--app/Statistics/Google/ChartNoChildrenFamilies.php177
-rw-r--r--app/Statistics/Google/ChartSex.php134
-rw-r--r--app/Statistics/Helper/Century.php46
-rw-r--r--app/Statistics/Helper/Country.php802
-rw-r--r--app/Statistics/Helper/Sql.php49
-rw-r--r--app/Statistics/Repository/BrowserRepository.php56
-rw-r--r--app/Statistics/Repository/ContactRepository.php82
-rw-r--r--app/Statistics/Repository/EventRepository.php430
-rw-r--r--app/Statistics/Repository/FamilyDatesRepository.php444
-rw-r--r--app/Statistics/Repository/FamilyRepository.php1863
-rw-r--r--app/Statistics/Repository/FavoritesRepository.php105
-rw-r--r--app/Statistics/Repository/GedcomRepository.php183
-rw-r--r--app/Statistics/Repository/HitCountRepository.php143
-rw-r--r--app/Statistics/Repository/IndividualRepository.php2027
-rw-r--r--app/Statistics/Repository/Interfaces/BrowserRepositoryInterface.php45
-rw-r--r--app/Statistics/Repository/Interfaces/ContactRepositoryInterface.php38
-rw-r--r--app/Statistics/Repository/Interfaces/EventRepositoryInterface.php166
-rw-r--r--app/Statistics/Repository/Interfaces/FamilyDatesRepositoryInterface.php248
-rw-r--r--app/Statistics/Repository/Interfaces/FavoritesRepositoryInterface.php52
-rw-r--r--app/Statistics/Repository/Interfaces/GedcomRepositoryInterface.php80
-rw-r--r--app/Statistics/Repository/Interfaces/HitCountRepositoryInterface.php96
-rw-r--r--app/Statistics/Repository/Interfaces/IndividualRepositoryInterface.php174
-rw-r--r--app/Statistics/Repository/Interfaces/LatestUserRepositoryInterface.php73
-rw-r--r--app/Statistics/Repository/Interfaces/MediaRepositoryInterface.php175
-rw-r--r--app/Statistics/Repository/Interfaces/MessageRepositoryInterface.php31
-rw-r--r--app/Statistics/Repository/Interfaces/NewsRepositoryInterface.php38
-rw-r--r--app/Statistics/Repository/Interfaces/PlaceRepositoryInterface.php86
-rw-r--r--app/Statistics/Repository/Interfaces/ServerRepositoryInterface.php52
-rw-r--r--app/Statistics/Repository/Interfaces/UserRepositoryInterface.php103
-rw-r--r--app/Statistics/Repository/LatestUserRepository.php130
-rw-r--r--app/Statistics/Repository/MediaRepository.php365
-rw-r--r--app/Statistics/Repository/MessageRepository.php41
-rw-r--r--app/Statistics/Repository/NewsRepository.php69
-rw-r--r--app/Statistics/Repository/PlaceRepository.php357
-rw-r--r--app/Statistics/Repository/ServerRepository.php60
-rw-r--r--app/Statistics/Repository/UserRepository.php266
-rw-r--r--app/Stats.php6761
-rw-r--r--resources/views/modules/statistics-chart/families.phtml154
-rw-r--r--resources/views/modules/statistics-chart/individuals.phtml144
-rw-r--r--resources/views/modules/statistics-chart/other.phtml95
-rw-r--r--resources/views/statistics/families/age-difference.phtml55
-rw-r--r--resources/views/statistics/families/birth-age.phtml61
-rw-r--r--resources/views/statistics/families/children.phtml59
-rw-r--r--resources/views/statistics/families/marriage-age.phtml71
-rw-r--r--resources/views/statistics/families/marriage-length.phtml37
-rw-r--r--resources/views/statistics/families/top10-list-age.phtml28
-rw-r--r--resources/views/statistics/families/top10-list-grand.phtml21
-rw-r--r--resources/views/statistics/families/top10-list-spouses.phtml18
-rw-r--r--resources/views/statistics/families/top10-list.phtml21
-rw-r--r--resources/views/statistics/families/top10-nolist-age.phtml24
-rw-r--r--resources/views/statistics/families/top10-nolist-grand.phtml15
-rw-r--r--resources/views/statistics/families/top10-nolist-spouses.phtml13
-rw-r--r--resources/views/statistics/families/top10-nolist.phtml15
-rw-r--r--resources/views/statistics/families/total-records.phtml112
-rw-r--r--resources/views/statistics/hit-count.phtml8
-rw-r--r--resources/views/statistics/individuals/greatest-age.phtml33
-rw-r--r--resources/views/statistics/individuals/lifespan.phtml52
-rw-r--r--resources/views/statistics/individuals/names.phtml63
-rw-r--r--resources/views/statistics/individuals/oldest-living.phtml33
-rw-r--r--resources/views/statistics/individuals/top10-list.phtml18
-rw-r--r--resources/views/statistics/individuals/top10-nolist.phtml13
-rw-r--r--resources/views/statistics/individuals/total-events.phtml111
-rw-r--r--resources/views/statistics/individuals/total-records.phtml77
-rw-r--r--resources/views/statistics/other/chart-distribution.phtml31
-rw-r--r--resources/views/statistics/other/chart-google.phtml1
-rw-r--r--resources/views/statistics/other/chart-objects.phtml23
-rw-r--r--resources/views/statistics/other/chart-sources.phtml39
-rw-r--r--resources/views/statistics/other/places.phtml53
-rw-r--r--resources/views/statistics/other/top10-list.phtml19
-rw-r--r--resources/views/statistics/other/total-events.phtml38
-rw-r--r--resources/views/statistics/other/total-records.phtml52
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&amp;chs=' . $sizes[0] . 'x' . $sizes[1]
+ . '&amp;chm=D,FF0000,2,0,3,1|N*f1*,000000,0,-1,11,1|N*f1*,000000,1,-1,11,1&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt='
+ . rawurlencode($chtt) . '&amp;chd=' . $chd . '&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl='
+ . rawurlencode($chxl) . '&amp;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&amp;chs=' . $sizes[0] . 'x' . $sizes[1]
+ . '&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chm=D,FF0000,0,0,3,1|' . $chm
+ . '&amp;chd=e:' . $chd . '&amp;chco=0000FF&amp;chbh=30,3&amp;chxt=x,x,y,y&amp;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&amp;chtm=' . $chart_shows;
+ $chart_url .= '&amp;chco=' . $chart_color1 . ',' . $chart_color3 . ',' . $chart_color2; // country colours
+ $chart_url .= '&amp;chf=bg,s,ECF5FF'; // sea colour
+ $chart_url .= '&amp;chs=' . $map_x . 'x' . $map_y;
+ $chart_url .= '&amp;chld=' . implode('', array_keys($surn_countries)) . '&amp;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&amp;chs=' . $sizes[0] . 'x' . $sizes[1]
+ . '&amp;chm=D,FF0000,2,0,3,1|' . $chmm . $chmf
+ . '&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=' . rawurlencode($chtt)
+ . '&amp;chd=' . $chd . '&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl='
+ . rawurlencode($chxl) . '&amp;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&amp;chs=' . $sizes[0] . 'x' . $sizes[1]
+ . '&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chm=D,FF0000,0,0:'
+ . ($i - 1) . ',3,1|' . $chm . '&amp;chd=e:'
+ . $chd . '&amp;chco=0000FF,ffffff00&amp;chbh=30,3&amp;chxt=x,x,y,y&amp;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([
+// '[',
+// ']',
+// '(',
+// ')',
+// '+',
+// ], [
+// '&rlm;[',
+// '&rlm;]',
+// '&rlm;(',
+// '&rlm;)',
+// '&rlm;+',
+// ], $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([
+ '[',
+ ']',
+ '(',
+ ')',
+ '+',
+ ], [
+ '&rlm;[',
+ '&rlm;]',
+ '&rlm;(',
+ '&rlm;)',
+ '&rlm;+',
+ ], $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([
+// '[',
+// ']',
+// '(',
+// ')',
+// '+',
+// ], [
+// '&rlm;[',
+// '&rlm;]',
+// '&rlm;(',
+// '&rlm;)',
+// '&rlm;+',
+// ], $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([
+// '[',
+// ']',
+// '(',
+// ')',
+// '+',
+// ], [
+// '&rlm;[',
+// '&rlm;]',
+// '&rlm;(',
+// '&rlm;)',
+// '&rlm;+',
+// ], $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([
+ '[',
+ ']',
+ '(',
+ ')',
+ '+',
+ ], [
+ '&rlm;[',
+ '&rlm;]',
+ '&rlm;(',
+ '&rlm;)',
+ '&rlm;+',
+ ], $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([
+// '[',
+// ']',
+// '(',
+// ')',
+// '+',
+// ], [
+// '&rlm;[',
+// '&rlm;]',
+// '&rlm;(',
+// '&rlm;)',
+// '&rlm;+',
+// ], $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([
+// '[',
+// ']',
+// '(',
+// ')',
+// '+',
+// ], [
+// '&rlm;[',
+// '&rlm;]',
+// '&rlm;(',
+// '&rlm;)',
+// '&rlm;+',
+// ], $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&amp;chd=e:' . $chd . '&amp;chs=' . $size . '&amp;chco=' . $color_from . ',' . $color_to . '&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_unknown},{$color_female},{$color_male}&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_female},{$color_male}&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_living},{$color_dead}&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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&amp;chtm=' . $chart_shows;
- $chart_url .= '&amp;chco=' . $WT_STATS_CHART_COLOR1 . ',' . $WT_STATS_CHART_COLOR3 . ',' . $WT_STATS_CHART_COLOR2; // country colours
- $chart_url .= '&amp;chf=bg,s,ECF5FF'; // sea colour
- $chart_url .= '&amp;chs=' . $WT_STATS_MAP_X . 'x' . $WT_STATS_MAP_Y;
- $chart_url .= '&amp;chld=' . implode('', array_keys($surn_countries)) . '&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chm=D,FF0000,2,0,3,1|N*f1*,000000,0,-1,11,1|N*f1*,000000,1,-1,11,1&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=" . rawurlencode($chtt) . "&amp;chd={$chd}&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . '&amp;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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chm=D,FF0000,2,0,3,1|{$chmm}{$chmf}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chtt=" . rawurlencode($chtt) . "&amp;chd={$chd}&amp;chco=0000FF,FFA0CB,FF0000&amp;chbh=20,3&amp;chxt=x,x,y,y&amp;chxl=" . rawurlencode($chxl) . '&amp;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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chm=D,FF0000,0,0,3,1|{$chm}&amp;chd=e:{$chd}&amp;chco=0000FF&amp;chbh=30,3&amp;chxt=x,x,y,y&amp;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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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&amp;chs={$sizes[0]}x{$sizes[1]}&amp;chf=bg,s,ffffff00|c,s,ffffff00&amp;chm=D,FF0000,0,0:" . ($i - 1) . ",3,1|{$chm}&amp;chd=e:{$chd}&amp;chco=0000FF,ffffff00&amp;chbh=30,3&amp;chxt=x,x,y,y&amp;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([
- '[',
- ']',
- '(',
- ')',
- '+',
- ], [
- '&rlm;[',
- '&rlm;]',
- '&rlm;(',
- '&rlm;)',
- '&rlm;+',
- ], $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&amp;chd=e:' . $chd . '&amp;chs=' . $size . '&amp;chco=' . $color_from . ',' . $color_to . '&amp;chf=bg,s,ffffff00&amp;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&amp;chd=e:{$chd}&amp;chs={$size}&amp;chco={$color_from},{$color_to}&amp;chf=bg,s,ffffff00&amp;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>