/** * Get a list of actual surnames and variants, based on a "root" surname. * * @param Tree $tree only fetch individuals from this tree * @param string $surn if set, only fetch people with this surname * @param string $salpha if set, only consider surnames starting with this letter * @param bool $marnm if set, include married names * @param bool $fams if set, only consider individuals with FAMS records * * @return array */ public static function surnames(Tree $tree, $surn, $salpha, $marnm, $fams) { $sql = "SELECT SQL_CACHE n2.n_surn, n1.n_surname, n1.n_id" . " FROM `##name` n1 " . ($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") . " JOIN (SELECT n_surn COLLATE :collate_0 AS n_surn, n_file FROM `##name`" . " WHERE n_file = :tree_id" . ($marnm ? "" : " AND n_type != '_MARNM'"); $args = array('tree_id' => $tree->getTreeId(), 'collate_0' => I18N::collation()); if ($surn) { $sql .= " AND n_surn COLLATE :collate_1 = :surn"; $args['collate_1'] = I18N::collation(); $args['surn'] = $surn; } elseif ($salpha === ',') { $sql .= " AND n_surn = ''"; } elseif ($salpha === '@') { $sql .= " AND n_surn = '@N.N.'"; } elseif ($salpha) { $sql .= " AND " . self::getInitialSql('n_surn', $salpha); } else { // All surnames $sql .= " AND n_surn NOT IN ('', '@N.N.')"; } $sql .= " GROUP BY n_surn COLLATE :collate_2, n_file) AS n2 ON (n1.n_surn = n2.n_surn COLLATE :collate_3 AND n1.n_file = n2.n_file)"; $args['collate_2'] = I18N::collation(); $args['collate_3'] = I18N::collation(); $list = array(); foreach (Database::prepare($sql)->execute($args)->fetchAll() as $row) { $list[I18N::strtoupper($row->n_surn)][$row->n_surname][$row->n_id] = true; } return $list; }
/** * Generate the HTML content of this block. * * @param int $block_id * @param bool $template * @param string[] $cfg * * @return string */ public function getBlock($block_id, $template = true, $cfg = array()) { global $WT_TREE, $ctype; $COMMON_NAMES_REMOVE = $WT_TREE->getPreference('COMMON_NAMES_REMOVE'); $COMMON_NAMES_THRESHOLD = $WT_TREE->getPreference('COMMON_NAMES_THRESHOLD'); $num = $this->getBlockSetting($block_id, 'num', '10'); $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', 'table'); $block = $this->getBlockSetting($block_id, 'block', '0'); foreach (array('num', 'infoStyle', 'block') as $name) { if (array_key_exists($name, $cfg)) { ${$name} = $cfg[$name]; } } // This next function is a bit out of date, and doesn't cope well with surname variants $top_surnames = FunctionsDb::getTopSurnames($WT_TREE->getTreeId(), $COMMON_NAMES_THRESHOLD, $num); // Remove names found in the "Remove Names" list if ($COMMON_NAMES_REMOVE) { foreach (preg_split("/[,; ]+/", $COMMON_NAMES_REMOVE) as $delname) { unset($top_surnames[$delname]); unset($top_surnames[I18N::strtoupper($delname)]); } } $all_surnames = array(); $i = 0; foreach (array_keys($top_surnames) as $top_surname) { $all_surnames = array_merge($all_surnames, QueryName::surnames($WT_TREE, $top_surname, '', false, false)); if (++$i == $num) { break; } } if ($i < $num) { $num = $i; } $id = $this->getName() . $block_id; $class = $this->getName() . '_block'; if ($ctype === 'gedcom' && Auth::isManager($WT_TREE) || $ctype === 'user' && Auth::check()) { $title = '<a class="icon-admin" title="' . I18N::translate('Configure') . '" href="block_edit.php?block_id=' . $block_id . '&ged=' . $WT_TREE->getNameHtml() . '&ctype=' . $ctype . '"></a>'; } else { $title = ''; } if ($num == 1) { // I18N: i.e. most popular surname. $title .= I18N::translate('Top surname'); } else { // I18N: Title for a list of the most common surnames, %s is a number. Note that a separate translation exists when %s is 1 $title .= I18N::plural('Top %s surname', 'Top %s surnames', $num, I18N::number($num)); } switch ($infoStyle) { case 'tagcloud': uksort($all_surnames, '\\Fisharebest\\Webtrees\\I18N::strcasecmp'); $content = FunctionsPrintLists::surnameTagCloud($all_surnames, 'indilist.php', true, $WT_TREE); break; case 'list': uasort($all_surnames, '\\Fisharebest\\Webtrees\\Module\\TopSurnamesModule::surnameCountSort'); $content = FunctionsPrintLists::surnameList($all_surnames, '1', true, 'indilist.php', $WT_TREE); break; case 'array': uasort($all_surnames, '\\Fisharebest\\Webtrees\\Module\\TopSurnamesModule::surnameCountSort'); $content = FunctionsPrintLists::surnameList($all_surnames, '2', true, 'indilist.php', $WT_TREE); break; case 'table': default: uasort($all_surnames, '\\Fisharebest\\Webtrees\\Module\\TopSurnamesModule::surnameCountSort'); $content = FunctionsPrintLists::surnameTable($all_surnames, 'indilist.php', $WT_TREE); break; } if ($template) { if ($block) { $class .= ' small_inner_block'; } return Theme::theme()->formatBlock($id, $title, $class, $content); } else { return $content; } }
/** * Create a chart of common surnames. * * @param string[] $params * * @return string */ public function chartCommonSurnames($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); if (isset($params[0]) && $params[0] != '') { $size = strtolower($params[0]); } else { $size = $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y; } if (isset($params[1]) && $params[1] != '') { $color_from = strtolower($params[1]); } else { $color_from = $WT_STATS_CHART_COLOR1; } if (isset($params[2]) && $params[2] != '') { $color_to = strtolower($params[2]); } else { $color_to = $WT_STATS_CHART_COLOR2; } if (isset($params[3]) && $params[3] != '') { $threshold = strtolower($params[3]); } else { $threshold = $this->tree->getPreference('COMMON_NAMES_THRESHOLD'); } if (isset($params[4]) && $params[4] != '') { $maxtoshow = strtolower($params[4]); } else { $maxtoshow = 7; } $sizes = explode('x', $size); $tot_indi = $this->totalIndividualsQuery(); $surnames = FunctionsDb::getCommonSurnames($threshold, $this->tree); if (count($surnames) <= 0) { return ''; } $SURNAME_TRADITION = $this->tree->getPreference('SURNAME_TRADITION'); uasort($surnames, '\\Fisharebest\\Webtrees\\Stats::nameTotalReverseSort'); $surnames = array_slice($surnames, 0, $maxtoshow); $all_surnames = array(); foreach (array_keys($surnames) as $n => $surname) { if ($n >= $maxtoshow) { break; } $all_surnames = array_merge($all_surnames, QueryName::surnames($this->tree, I18N::strtoupper($surname), '', false, false)); } $tot = 0; foreach ($surnames as $surname) { $tot += $surname['match']; } $chd = ''; $chl = array(); foreach ($all_surnames as $surns) { $count_per = 0; $max_name = 0; $top_name = ''; foreach ($surns as $spfxsurn => $indis) { $per = count($indis); $count_per += $per; // select most common surname from all variants if ($per > $max_name) { $max_name = $per; $top_name = $spfxsurn; } } switch ($SURNAME_TRADITION) { case 'polish': // most common surname should be in male variant (Kowalski, not Kowalska) $top_name = preg_replace(array('/ska$/', '/cka$/', '/dzka$/', '/żka$/'), array('ski', 'cki', 'dzki', 'żki'), $top_name); } $per = round(100 * $count_per / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = $top_name . ' - ' . I18N::number($count_per); } $per = round(100 * ($tot_indi - $tot) / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); $chart_title = implode(I18N::$list_separator, $chl); $chl = implode('|', $chl); return '<img src="https://chart.googleapis.com/chart?cht=p3&chd=e:' . $chd . '&chs=' . $size . '&chco=' . $color_from . ',' . $color_to . '&chf=bg,s,ffffff00&chl=' . rawurlencode($chl) . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . $chart_title . '" title="' . $chart_title . '" />'; }
/** * Print a list of surnames. * * @param string[][] $surnames array (of SURN, of array of SPFX_SURN, of array of PID) * @param int $style 1=bullet list, 2=semicolon-separated list, 3=tabulated list with up to 4 columns * @param bool $totals show totals after each name * @param string $script indilist or famlist * @param Tree $tree Link back to the individual list in this tree * * @return string */ public static function surnameList($surnames, $style, $totals, $script, Tree $tree) { $html = array(); foreach ($surnames as $surn => $surns) { // Each surname links back to the indilist if ($surn) { $url = $script . '?surname=' . urlencode($surn) . '&ged=' . $tree->getNameUrl(); } else { $url = $script . '?alpha=,&ged=' . $tree->getNameUrl(); } // If all the surnames are just case variants, then merge them into one // Comment out this block if you want SMITH listed separately from Smith $first_spfxsurn = null; foreach ($surns as $spfxsurn => $indis) { if ($first_spfxsurn) { if (I18N::strtoupper($spfxsurn) == I18N::strtoupper($first_spfxsurn)) { $surns[$first_spfxsurn] = array_merge($surns[$first_spfxsurn], $surns[$spfxsurn]); unset($surns[$spfxsurn]); } } else { $first_spfxsurn = $spfxsurn; } } $subhtml = '<a href="' . $url . '" dir="auto">' . Filter::escapeHtml(implode(I18N::$list_separator, array_keys($surns))) . '</a>'; if ($totals) { $subtotal = 0; foreach ($surns as $indis) { $subtotal += count($indis); } $subhtml .= ' (' . I18N::number($subtotal) . ')'; } $html[] = $subhtml; } switch ($style) { case 1: return '<ul><li>' . implode('</li><li>', $html) . '</li></ul>'; case 2: return implode(I18N::$list_separator, $html); case 3: $i = 0; $count = count($html); if ($count > 36) { $col = 4; } elseif ($count > 18) { $col = 3; } elseif ($count > 6) { $col = 2; } else { $col = 1; } $newcol = ceil($count / $col); $html2 = '<table class="list_table"><tr>'; $html2 .= '<td class="list_value" style="padding: 14px;">'; foreach ($html as $surns) { $html2 .= $surns . '<br>'; $i++; if ($i == $newcol && $i < $count) { $html2 .= '</td><td class="list_value" style="padding: 14px;">'; $newcol = $i + ceil($count / $col); } } $html2 .= '</td></tr></table>'; return $html2; } }
/** * Get array of common surnames * * This function returns a simple array of the most common surnames * found in the individuals list. * * @param int $min The number of times a surname must occur before it is added to the array * @param Tree $tree * * @return mixed[][] */ public static function getCommonSurnames($min, Tree $tree) { $COMMON_NAMES_ADD = $tree->getPreference('COMMON_NAMES_ADD'); $COMMON_NAMES_REMOVE = $tree->getPreference('COMMON_NAMES_REMOVE'); $topsurns = self::getTopSurnames($tree->getTreeId(), $min, 0); foreach (explode(',', $COMMON_NAMES_ADD) as $surname) { if ($surname && !array_key_exists($surname, $topsurns)) { $topsurns[$surname] = $min; } } foreach (explode(',', $COMMON_NAMES_REMOVE) as $surname) { unset($topsurns[I18N::strtoupper($surname)]); } //-- check if we found some, else recurse if (empty($topsurns) && $min > 2) { return self::getCommonSurnames($min / 2, $tree); } else { uksort($topsurns, '\\Fisharebest\\Webtrees\\I18N::strcasecmp'); foreach ($topsurns as $key => $value) { $topsurns[$key] = array('name' => $key, 'match' => $value); } return $topsurns; } }
/** * Create a chart of common surnames. * * @param string[] $params * * @return string */ public function chartCommonSurnames($params = array()) { $WT_STATS_CHART_COLOR1 = Theme::theme()->parameter('distribution-chart-no-values'); $WT_STATS_CHART_COLOR2 = Theme::theme()->parameter('distribution-chart-high-values'); $WT_STATS_S_CHART_X = Theme::theme()->parameter('stats-small-chart-x'); $WT_STATS_S_CHART_Y = Theme::theme()->parameter('stats-small-chart-y'); $size = empty($params[0]) ? $WT_STATS_S_CHART_X . "x" . $WT_STATS_S_CHART_Y : strtolower($params[0]); $color_from = empty($params[1]) ? $WT_STATS_CHART_COLOR1 : strtolower($params[1]); $color_to = empty($params[2]) ? $WT_STATS_CHART_COLOR2 : strtolower($params[2]); $number_of_surnames = empty($params[3]) ? 10 : (int) $params[3]; $sizes = explode('x', $size); $tot_indi = $this->totalIndividualsQuery(); $surnames = FunctionsDb::getTopSurnames($this->tree->getTreeId(), 0, $number_of_surnames); if (empty($surnames)) { return ''; } $SURNAME_TRADITION = $this->tree->getPreference('SURNAME_TRADITION'); $all_surnames = array(); $tot = 0; foreach ($surnames as $surname => $num) { $all_surnames = array_merge($all_surnames, QueryName::surnames($this->tree, I18N::strtoupper($surname), '', false, false)); $tot += $num; } $chd = ''; $chl = array(); foreach ($all_surnames as $surns) { $count_per = 0; $max_name = 0; $top_name = ''; foreach ($surns as $spfxsurn => $indis) { $per = count($indis); $count_per += $per; // select most common surname from all variants if ($per > $max_name) { $max_name = $per; $top_name = $spfxsurn; } } switch ($SURNAME_TRADITION) { case 'polish': // most common surname should be in male variant (Kowalski, not Kowalska) $top_name = preg_replace(array('/ska$/', '/cka$/', '/dzka$/', '/żka$/'), array('ski', 'cki', 'dzki', 'żki'), $top_name); } $per = round(100 * $count_per / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = $top_name . ' - ' . I18N::number($count_per); } $per = round(100 * ($tot_indi - $tot) / $tot_indi, 0); $chd .= $this->arrayToExtendedEncoding(array($per)); $chl[] = I18N::translate('Other') . ' - ' . I18N::number($tot_indi - $tot); $chart_title = implode(I18N::$list_separator, $chl); $chl = implode('|', $chl); return '<img src="https://chart.googleapis.com/chart?cht=p3&chd=e:' . $chd . '&chs=' . $size . '&chco=' . $color_from . ',' . $color_to . '&chf=bg,s,ffffff00&chl=' . rawurlencode($chl) . '" width="' . $sizes[0] . '" height="' . $sizes[1] . '" alt="' . $chart_title . '" title="' . $chart_title . '" />'; }
/** * Generate the HTML content of this block. * * @param int $block_id * @param bool $template * @param string[] $cfg * * @return string */ public function getBlock($block_id, $template = true, $cfg = array()) { global $WT_TREE, $ctype; $COMMON_NAMES_REMOVE = $WT_TREE->getPreference('COMMON_NAMES_REMOVE'); $COMMON_NAMES_THRESHOLD = $WT_TREE->getPreference('COMMON_NAMES_THRESHOLD'); $num = $this->getBlockSetting($block_id, 'num', '10'); // The “Minimum number of occurrences” input field from Control Panel’s Preferences page // has to be copied here otherwise user doesn't understand why the result is different // than expected (input of add and remove surnames may also be copied to make this module // indenpendent from module statistics; it is not copied yet). The data of original input // field is still used in the bottom of the statistics module $threshold = $this->getBlockSetting($block_id, 'threshold', '5'); $infoStyle = $this->getBlockSetting($block_id, 'infoStyle', 'table'); $block = $this->getBlockSetting($block_id, 'block', '0'); foreach (array('num', 'infoStyle', 'block') as $name) { if (array_key_exists($name, $cfg)) { ${$name} = $cfg[$name]; } } // This next function is a bit out of date, and doesn't cope well with surname variants // First defining the upper limit of surname occurrences to get at least one row in // result list if user sets too high number in “Minimum number of occurrences” field. // Without this definition a short message “No data available in table” appears by // webtrees\packages\datatables-1.10.7\js\jquery.dataTables.min.js JavaScript file // without the possibility of translating (and only if presentation style is table; // all other styles result no message at all) $top_surnames_in_DB = FunctionsDb::getCommonSurnames($WT_TREE->getPreference('COMMON_NAMES_THRESHOLD'), $WT_TREE); $max_occurrences = 0; foreach ($top_surnames_in_DB as $array) { if ($array['match'] > $max_occurrences) { $max_occurrences = $array['match']; } } if ($threshold > $max_occurrences) { $threshold = $max_occurrences; } $top_surnames = FunctionsDb::getTopSurnames($WT_TREE->getTreeId(), $threshold, $num); // Remove names found in the "Remove Names" list if ($COMMON_NAMES_REMOVE) { foreach (preg_split("/[,; ]+/", $COMMON_NAMES_REMOVE) as $delname) { unset($top_surnames[$delname]); unset($top_surnames[I18N::strtoupper($delname)]); } } $all_surnames = array(); $i = 0; foreach (array_keys($top_surnames) as $top_surname) { $all_surnames = array_merge($all_surnames, QueryName::surnames($WT_TREE, $top_surname, '', false, false)); if (++$i == $num) { break; } } if ($i < $num) { $num = $i; } $id = $this->getName() . $block_id; $class = $this->getName() . '_block'; if ($ctype === 'gedcom' && Auth::isManager($WT_TREE) || $ctype === 'user' && Auth::check()) { $title = '<a class="icon-admin" title="' . I18N::translate('Configure') . '" href="block_edit.php?block_id=' . $block_id . '&ged=' . $WT_TREE->getNameHtml() . '&ctype=' . $ctype . '"></a>'; } else { $title = ''; } if ($num == 1) { // I18N: i.e. most popular surname. $title .= I18N::translate('Top surname'); } else { // I18N: Title for a list of the most common surnames, %s is a number. Note that a separate translation exists when %s is 1 $title .= I18N::plural('Top %s surname', 'Top %s surnames', $num, I18N::number($num)); } switch ($infoStyle) { case 'tagcloud': uksort($all_surnames, '\\Fisharebest\\Webtrees\\I18N::strcasecmp'); $content = FunctionsPrintLists::surnameTagCloud($all_surnames, 'indilist.php', true, $WT_TREE); break; case 'list': uasort($all_surnames, '\\Fisharebest\\Webtrees\\Module\\TopSurnamesModule::surnameCountSort'); $content = FunctionsPrintLists::surnameList($all_surnames, '1', true, 'indilist.php', $WT_TREE); break; case 'array': uasort($all_surnames, '\\Fisharebest\\Webtrees\\Module\\TopSurnamesModule::surnameCountSort'); $content = FunctionsPrintLists::surnameList($all_surnames, '2', true, 'indilist.php', $WT_TREE); break; case 'table': default: uasort($all_surnames, '\\Fisharebest\\Webtrees\\Module\\TopSurnamesModule::surnameCountSort'); $content = FunctionsPrintLists::surnameTable($all_surnames, 'indilist.php', $WT_TREE); break; } if ($template) { if ($block) { $class .= ' small_inner_block'; } return Theme::theme()->formatBlock($id, $title, $class, $content); } else { return $content; } }
/** * Search the repositories * * @param string[] $query Search terms * @param Tree[] $trees The trees to search * * @return Repository[] */ public static function searchRepositories(array $query, array $trees) { // Convert the query into a regular expression $queryregex = array(); $sql = "SELECT o_id AS xref, o_file AS gedcom_id, o_gedcom AS gedcom FROM `##other` WHERE o_type = 'REPO'"; $args = array(); foreach ($query as $n => $q) { $queryregex[] = preg_quote(I18N::strtoupper($q), '/'); $sql .= " AND o_gedcom COLLATE :collate_" . $n . " LIKE CONCAT('%', :query_" . $n . ", '%')"; $args['collate_' . $n] = I18N::collation(); $args['query_' . $n] = Filter::escapeLike($q); } $sql .= " AND o_file IN ("; foreach ($trees as $n => $tree) { $sql .= $n ? ", " : ""; $sql .= ":tree_id_" . $n; $args['tree_id_' . $n] = $tree->getTreeId(); } $sql .= ")"; $list = array(); $rows = Database::prepare($sql)->execute($args)->fetchAll(); foreach ($rows as $row) { // SQL may have matched on private data or gedcom tags, so check again against privatized data. $record = Repository::getInstance($row->xref, Tree::findById($row->gedcom_id), $row->gedcom); // Ignore non-genealogy data $gedrec = preg_replace('/\\n\\d (_UID|_WT_USER|FILE|FORM|TYPE|CHAN|RESN) .*/', '', $record->getGedcom()); // Ignore links and tags $gedrec = preg_replace('/\\n\\d ' . WT_REGEX_TAG . '( @' . WT_REGEX_XREF . '@)?/', '', $gedrec); // Ignore tags $gedrec = preg_replace('/\\n\\d ' . WT_REGEX_TAG . ' ?/', '', $gedrec); // Re-apply the filtering $gedrec = I18N::strtoupper($gedrec); foreach ($queryregex as $regex) { if (!preg_match('/' . $regex . '/', $gedrec)) { continue 2; } } $list[] = $record; } $list = array_filter($list, function (Repository $x) { return $x->canShowName(); }); return $list; }