1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2016 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees\Module; 17 18use Fisharebest\Webtrees\Census\Census; 19use Fisharebest\Webtrees\Census\CensusInterface; 20use Fisharebest\Webtrees\Controller\SimpleController; 21use Fisharebest\Webtrees\Family; 22use Fisharebest\Webtrees\Filter; 23use Fisharebest\Webtrees\Functions\Functions; 24use Fisharebest\Webtrees\Functions\FunctionsDb; 25use Fisharebest\Webtrees\GedcomRecord; 26use Fisharebest\Webtrees\GedcomTag; 27use Fisharebest\Webtrees\I18N; 28use Fisharebest\Webtrees\Individual; 29use Fisharebest\Webtrees\Menu; 30use Fisharebest\Webtrees\Note; 31use Fisharebest\Webtrees\Soundex; 32 33/** 34 * Class CensusAssistantModule 35 */ 36class CensusAssistantModule extends AbstractModule { 37 /** {@inheritdoc} */ 38 public function getTitle() { 39 return /* I18N: Name of a module */ I18N::translate('Census assistant'); 40 } 41 42 /** {@inheritdoc} */ 43 public function getDescription() { 44 return /* I18N: Description of the “Census assistant” module */ I18N::translate('An alternative way to enter census transcripts and link them to individuals.'); 45 } 46 47 /** 48 * This is a general purpose hook, allowing modules to respond to routes 49 * of the form module.php?mod=FOO&mod_action=BAR 50 * 51 * @param string $mod_action 52 */ 53 public function modAction($mod_action) { 54 switch ($mod_action) { 55 case 'census_find': 56 self::censusFind(); 57 break; 58 case 'media_find': 59 self::mediaFind(); 60 break; 61 case 'media_query_3a': 62 self::mediaQuery(); 63 break; 64 default: 65 http_response_code(404); 66 } 67 } 68 69 /** 70 * Find an individual. 71 */ 72 private static function censusFind() { 73 global $WT_TREE; 74 75 $controller = new SimpleController; 76 $filter = Filter::get('filter'); 77 $action = Filter::get('action'); 78 $census = Filter::get('census'); 79 $census = new $census; 80 81 $controller 82 ->restrictAccess($census instanceof CensusInterface) 83 ->setPageTitle(I18N::translate('Find an individual')) 84 ->pageHeader(); 85 86 echo '<table class="list_table width90" border="0">'; 87 echo '<tr><td style="padding: 10px;" class="facts_label03 width90">'; 88 echo I18N::translate('Find an individual'); 89 echo '</td>'; 90 echo '</table>'; 91 echo '<br>'; 92 93 if ($action == 'filter') { 94 $filter = trim($filter); 95 $filter_array = explode(' ', preg_replace('/ {2,}/', ' ', $filter)); 96 97 // Output Individual for GEDFact Assistant ====================== 98 echo '<table class="list_table width90">'; 99 $myindilist = FunctionsDb::searchIndividualNames($filter_array, [$WT_TREE]); 100 if ($myindilist) { 101 echo '<tr><td class="list_value_wrap"><ul>'; 102 usort($myindilist, '\Fisharebest\Webtrees\GedcomRecord::compare'); 103 foreach ($myindilist as $indi) { 104 echo '<li>'; 105 echo '<a href="#" onclick="window.opener.appendCensusRow(\'' . Filter::escapeJs(self::censusTableRow($census, $indi, null)) . '\'); window.close();">'; 106 echo '<b>' . $indi->getFullName() . '</b>'; 107 echo '</a>'; 108 echo $indi->formatFirstMajorFact(WT_EVENTS_BIRT, 1); 109 echo $indi->formatFirstMajorFact(WT_EVENTS_DEAT, 1); 110 echo '<hr>'; 111 echo '</li>'; 112 } 113 echo '</ul></td></tr>'; 114 } else { 115 echo '<tr><td class="list_value_wrap">'; 116 echo I18N::translate('No results found.'); 117 echo '</td></tr>'; 118 } 119 echo '<tr><td>'; 120 echo '<button onclick="window.close();">', I18N::translate('close'), '</button>'; 121 echo '</td></tr>'; 122 echo '</table>'; 123 } 124 } 125 126 /** 127 * Find a media object. 128 */ 129 private static function mediaFind() { 130 global $WT_TREE; 131 132 $controller = new SimpleController; 133 $filter = Filter::get('filter'); 134 $multiple = Filter::getBool('multiple'); 135 136 $controller 137 ->setPageTitle(I18N::translate('Find an individual')) 138 ->pageHeader(); 139 140 ?> 141 <script> 142 function pasterow(id, name, gend, yob, age, bpl) { 143 window.opener.opener.insertRowToTable(id, name, '', gend, '', yob, age, 'Y', '', bpl); 144 } 145 146 function pasteid(id, name, thumb) { 147 if (thumb) { 148 window.opener.paste_id(id, name, thumb); 149 <?php if (!$multiple) { echo "window.close();"; } ?> 150 } else { 151 // GEDFact_assistant ======================== 152 if (window.opener.document.getElementById('addlinkQueue')) { 153 window.opener.insertRowToTable(id, name); 154 } 155 window.opener.paste_id(id); 156 if (window.opener.pastename) { 157 window.opener.pastename(name); 158 } 159 <?php if (!$multiple) { echo "window.close();"; } ?> 160 } 161 } 162 function checknames(frm) { 163 if (document.forms[0].subclick) { 164 button = document.forms[0].subclick.value; 165 } else { 166 button = ""; 167 } 168 if (frm.filter.value.length < 2 && button !== "all") { 169 alert("<?php echo I18N::translate('Please enter more than one character.'); ?>"); 170 frm.filter.focus(); 171 return false; 172 } 173 if (button=="all") { 174 frm.filter.value = ""; 175 } 176 return true; 177 } 178 </script> 179 180 <?php 181 echo '<div>'; 182 echo '<table class="list_table width90" border="0">'; 183 echo '<tr><td style="padding: 10px;" class="facts_label03 width90">'; // start column for find text header 184 echo $controller->getPageTitle(); 185 echo '</td>'; 186 echo '</tr>'; 187 echo '</table>'; 188 echo '<br>'; 189 echo '<button onclick="window.close();">', I18N::translate('close'), '</button>'; 190 echo '<br>'; 191 192 $filter = trim($filter); 193 $filter_array = explode(' ', preg_replace('/ {2,}/', ' ', $filter)); 194 echo '<table class="tabs_table width90"><tr>'; 195 $myindilist = FunctionsDb::searchIndividualNames($filter_array, [$WT_TREE]); 196 if ($myindilist) { 197 echo '<td class="list_value_wrap"><ul>'; 198 usort($myindilist, '\Fisharebest\Webtrees\GedcomRecord::compare'); 199 foreach ($myindilist as $indi) { 200 $nam = Filter::escapeHtml($indi->getFullName()); 201 echo "<li><a href=\"#\" onclick=\"pasterow( 202 '" . $indi->getXref() . "' , 203 '" . $nam . "' , 204 '" . $indi->getSex() . "' , 205 '" . $indi->getBirthYear() . "' , 206 '" . (1901 - $indi->getBirthYear()) . "' , 207 '" . $indi->getBirthPlace() . "'); return false;\"> 208 <b>" . $indi->getFullName() . "</b> "; 209 210 $born = GedcomTag::getLabel('BIRT'); 211 echo "</span><br><span class=\"list_item\">", $born, " ", $indi->getBirthYear(), " ", $indi->getBirthPlace(), "</span></a></li>"; 212 echo "<hr>"; 213 } 214 echo '</ul></td></tr><tr><td class="list_label">', I18N::translate('Total individuals: %s', count($myindilist)), '</tr></td>'; 215 } else { 216 echo "<td class=\"list_value_wrap\">"; 217 echo I18N::translate('No results found.'); 218 echo "</td></tr>"; 219 } 220 echo "</table>"; 221 echo '</div>'; 222 } 223 224 /** 225 * Search for a media object. 226 */ 227 private static function mediaQuery() { 228 global $WT_TREE; 229 230 $iid2 = Filter::get('iid', WT_REGEX_XREF); 231 232 $controller = new SimpleController; 233 $controller 234 ->setPageTitle(I18N::translate('Link to an existing media object')) 235 ->pageHeader(); 236 237 $record = GedcomRecord::getInstance($iid2, $WT_TREE); 238 if ($record) { 239 $headjs = ''; 240 if ($record instanceof Family) { 241 if ($record->getHusband()) { 242 $headjs = $record->getHusband()->getXref(); 243 } elseif ($record->getWife()) { 244 $headjs = $record->getWife()->getXref(); 245 } 246 } 247 ?> 248 <script> 249 function insertId() { 250 if (window.opener.document.getElementById('addlinkQueue')) { 251 // alert('Please move this alert window and examine the contents of the pop-up window, then click OK') 252 window.opener.insertRowToTable('<?php echo $record->getXref(); ?>', '<?php echo htmlspecialchars($record->getFullName()); ?>', '<?php echo $headjs; ?>'); 253 window.close(); 254 } 255 } 256 </script> 257 <?php 258 } else { 259 ?> 260 <script> 261 function insertId() { 262 window.opener.alert('<?php echo $iid2; ?> - <?php echo I18N::translate('Not a valid individual, family, or source ID'); ?>'); 263 window.close(); 264 } 265 </script> 266 <?php 267 } 268 ?> 269 <script>window.onLoad = insertId();</script> 270 <?php 271 } 272 273 /** 274 * Convert custom markup into HTML 275 * 276 * @param Note $note 277 * 278 * @return string 279 */ 280 public static function formatCensusNote(Note $note) { 281 global $WT_TREE; 282 283 if (preg_match('/(.*)((?:\n.*)*)\n\.start_formatted_area\.\n(.+)\n(.+(?:\n.+)*)\n.end_formatted_area\.((?:\n.*)*)/', $note->getNote(), $match)) { 284 // This looks like a census-assistant shared note 285 $title = Filter::escapeHtml($match[1]); 286 $preamble = Filter::escapeHtml($match[2]); 287 $header = Filter::escapeHtml($match[3]); 288 $data = Filter::escapeHtml($match[4]); 289 $postamble = Filter::escapeHtml($match[5]); 290 291 // Get the column headers for the census to which this note refers 292 // requires the fact place & date to match the specific census 293 // censusPlace() (Soundex match) and censusDate() functions 294 $fmt_headers = []; 295 $linkedRecords = array_merge($note->linkedIndividuals('NOTE'), $note->linkedFamilies('NOTE')); 296 $firstRecord = array_shift($linkedRecords); 297 if ($firstRecord) { 298 $countryCode = ''; 299 $date = ''; 300 foreach ($firstRecord->getFacts('CENS') as $fact) { 301 if (trim($fact->getAttribute('NOTE'), '@') === $note->getXref()) { 302 $date = $fact->getAttribute('DATE'); 303 $place = explode(',', strip_tags($fact->getPlace()->getFullName())); 304 $countryCode = Soundex::daitchMokotoff(array_pop($place)); 305 break; 306 } 307 } 308 309 foreach (Census::allCensusPlaces() as $censusPlace) { 310 if (Soundex::compare($countryCode, Soundex::daitchMokotoff($censusPlace->censusPlace()))) { 311 foreach ($censusPlace->allCensusDates() as $census) { 312 if ($census->censusDate() == $date) { 313 foreach ($census->columns() as $column) { 314 $abbrev = $column->abbreviation(); 315 if ($abbrev) { 316 $description = $column->title() ? $column->title() : I18N::translate('Description unavailable'); 317 $fmt_headers[$abbrev] = '<span title="' . $description . '">' . $abbrev . '</span>'; 318 } 319 } 320 break 2; 321 } 322 } 323 } 324 } 325 } 326 // Substitute header labels and format as HTML 327 $thead = '<tr><th>' . strtr(str_replace('|', '</th><th>', $header), $fmt_headers) . '</th></tr>'; 328 $thead = str_replace('.b.', '', $thead); 329 330 // Format data as HTML 331 $tbody = ''; 332 foreach (explode("\n", $data) as $row) { 333 $tbody .= '<tr>'; 334 foreach (explode('|', $row) as $column) { 335 $tbody .= '<td>' . $column . '</td>'; 336 } 337 $tbody .= '</tr>'; 338 } 339 340 return 341 $title . "\n" . // The newline allows the framework to expand the details and turn the first line into a link 342 '<div class="markdown">' . 343 '<p>' . $preamble . '</p>' . 344 '<table>' . 345 '<thead>' . $thead . '</thead>' . 346 '<tbody>' . $tbody . '</tbody>' . 347 '</table>' . 348 '<p>' . $postamble . '</p>' . 349 '</div>'; 350 } else { 351 // Not a census-assistant shared note - apply default formatting 352 return Filter::formatText($note->getNote(), $WT_TREE); 353 } 354 } 355 356 /** 357 * Generate an HTML row of data for the census header 358 * 359 * Add prefix cell (store XREF and drag/drop) 360 * Add suffix cell (delete button) 361 * 362 * @param CensusInterface $census 363 * 364 * @return string 365 */ 366 public static function censusTableHeader(CensusInterface $census) { 367 $html = ''; 368 foreach ($census->columns() as $column) { 369 $html .= '<th title="' . $column->title() . '">' . $column->abbreviation() . '</th>'; 370 } 371 372 return '<tr><th hidden></th>' . $html . '<th></th></tr>'; 373 } 374 375 /** 376 * Generate an HTML row of data for the census 377 * 378 * Add prefix cell (store XREF and drag/drop) 379 * Add suffix cell (delete button) 380 * 381 * @param CensusInterface $census 382 * 383 * @return string 384 */ 385 public static function censusTableEmptyRow(CensusInterface $census) { 386 return '<tr><td hidden></td>' . str_repeat('<td><input type="text"></td>', count($census->columns())) . '<td><a class="icon-remove" href="#" title="' . I18N::translate('Remove') . '"></a></td></tr>'; 387 } 388 389 /** 390 * Generate an HTML row of data for the census 391 * 392 * Add prefix cell (store XREF and drag/drop) 393 * Add suffix cell (delete button) 394 * 395 * @param CensusInterface $census 396 * @param Individual $individual 397 * @param Individual|null $head 398 * 399 * @return string 400 */ 401 public static function censusTableRow(CensusInterface $census, Individual $individual, Individual $head = null) { 402 $html = ''; 403 foreach ($census->columns() as $column) { 404 $html .= '<td><input type="text" value="' . $column->generate($individual, $head) . '"></td>'; 405 } 406 407 return '<tr><td hidden>' . $individual->getXref() . '</td>' . $html . '<td><a class="icon-remove" href="#" title="' . I18N::translate('Remove') . '"></a></td></tr>'; 408 } 409 410 /** 411 * Create a family on the census navigator. 412 * 413 * @param CensusInterface $census 414 * @param Family $family 415 * @param Individual $head 416 * 417 * @return string 418 */ 419 public static function censusNavigatorFamily(CensusInterface $census, Family $family, Individual $head) { 420 $headImg2 = '<i class="icon-button_head" title="' . I18N::translate('Head of household') . '"></i>'; 421 422 foreach ($family->getSpouses() as $spouse) { 423 $menu = new Menu(Functions::getCloseRelationshipName($head, $spouse)); 424 foreach ($spouse->getChildFamilies() as $grandparents) { 425 foreach ($grandparents->getSpouses() as $grandparent) { 426 $submenu = new Menu( 427 Functions::getCloseRelationshipName($head, $grandparent) . ' - ' . $grandparent->getFullName(), 428 '#', 429 '', 430 ['onclick' => 'return appendCensusRow("' . Filter::escapeJs(self::censusTableRow($census, $grandparent, $head)) . '");'] 431 ); 432 $submenu->addClass('submenuitem', ''); 433 $menu->addSubmenu($submenu); 434 $menu->addClass('', 'submenu'); 435 } 436 } 437 438 ?> 439 <tr> 440 <td class="optionbox"> 441 <?php echo $menu->getMenu(); ?> 442 </td> 443 <td class="facts_value nowrap"> 444 <a href="#" onclick="return appendCensusRow('<?php echo Filter::escapeJs(self::censusTableRow($census, $spouse, $head)); ?>');"> 445 <?php echo $spouse->getFullName(); ?> 446 </a> 447 </td> 448 <td class="facts_value"> 449 <?php if ($head !== $spouse): ?> 450 <a href="edit_interface.php?action=addnewnote_assisted&noteid=newnote&xref=<?php echo $spouse->getXref(); ?>&gedcom=<?php echo $spouse->getTree()->getNameUrl(); ?>&census=<?php echo get_class($census); ?>"> 451 <?php echo $headImg2; ?> 452 </a> 453 <?php endif; ?> 454 </td> 455 </tr> 456 <?php 457 } 458 459 foreach ($family->getChildren() as $child) { 460 $menu = new Menu(Functions::getCloseRelationshipName($head, $child)); 461 foreach ($child->getSpouseFamilies() as $spouse_family) { 462 foreach ($spouse_family->getSpouses() as $spouse_family_spouse) { 463 if ($spouse_family_spouse != $child) { 464 $submenu = new Menu( 465 Functions::getCloseRelationshipName($head, $spouse_family_spouse) . ' - ' . $spouse_family_spouse->getFullName(), 466 '#', 467 '', 468 ['onclick' => 'return appendCensusRow("' . Filter::escapeJs(self::censusTableRow($census, $spouse_family_spouse, $head)) . '");'] 469 ); 470 $submenu->addClass('submenuitem', ''); 471 $menu->addSubmenu($submenu); 472 $menu->addClass('', 'submenu'); 473 } 474 } 475 foreach ($spouse_family->getChildren() as $spouse_family_child) { 476 $submenu = new Menu( 477 Functions::getCloseRelationshipName($head, $spouse_family_child) . ' - ' . $spouse_family_child->getFullName(), 478 '#', 479 '', 480 ['onclick' => 'return appendCensusRow("' . Filter::escapeJs(self::censusTableRow($census, $spouse_family_child, $head)) . '");'] 481 ); 482 $submenu->addClass('submenuitem', ''); 483 $menu->addSubmenu($submenu); 484 $menu->addClass('', 'submenu'); 485 } 486 } 487 488 ?> 489 <tr> 490 <td class="optionbox"> 491 <?php echo $menu->getMenu(); ?> 492 </td> 493 <td class="facts_value"> 494 <a href="#" onclick="return appendCensusRow('<?php echo Filter::escapeJs(self::censusTableRow($census, $child, $head)); ?>');"> 495 <?php echo $child->getFullName(); ?> 496 </a> 497 </td> 498 <td class="facts_value"> 499 <?php if ($head !== $child): ?> 500 <a href="edit_interface.php?action=addnewnote_assisted&noteid=newnote&xref=<?php echo $child->getXref(); ?>&gedcom=<?php echo $child->getTree()->getNameUrl(); ?>&census=<?php echo get_class($census); ?>"> 501 <?php echo $headImg2; ?> 502 </a> 503 <?php endif; ?> 504 </td> 505 </tr> 506 <?php 507 } 508 echo '<tr><td><br></td></tr>'; 509 } 510} 511