xref: /webtrees/app/Module/CensusAssistantModule.php (revision 6664b4a34cf6b2d1fc123cfb8f05bb5dda4a7f25)
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, 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, 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>&nbsp;&nbsp;&nbsp;";
209
210				$born = GedcomTag::getLabel('BIRT');
211				echo "</span><br><span class=\"list_item\">", $born, " ", $indi->getBirthYear(), "&nbsp;&nbsp;&nbsp;", $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   = array();
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('Click to choose individual as head of family.') . '"></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						array('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					<a href="edit_interface.php?action=addnewnote_assisted&amp;noteid=newnote&amp;xref=<?php echo $spouse->getXref(); ?>&amp;gedcom=<?php echo $spouse->getTree()->getNameUrl(); ?>&amp;census=<?php echo get_class($census); ?>">
450						<?php echo $headImg2; ?>
451					</a>
452				</td>
453			</tr>
454			<?php
455		}
456
457		foreach ($family->getChildren() as $child) {
458			$menu = new Menu(Functions::getCloseRelationshipName($head, $child));
459			foreach ($child->getSpouseFamilies() as $spouse_family) {
460				foreach ($spouse_family->getSpouses() as $spouse_family_spouse) {
461					if ($spouse_family_spouse != $child) {
462						$submenu = new Menu(
463							Functions::getCloseRelationshipName($head, $spouse_family_spouse) . ' - ' . $spouse_family_spouse->getFullName(),
464							'#',
465							'',
466							array('onclick' => 'return appendCensusRow("' . Filter::escapeJs(self::censusTableRow($census, $spouse_family_spouse, $head)) . '");')
467						);
468						$submenu->addClass('submenuitem', '');
469						$menu->addSubmenu($submenu);
470						$menu->addClass('', 'submenu');
471					}
472				}
473				foreach ($spouse_family->getChildren() as $spouse_family_child) {
474					$submenu = new Menu(
475						Functions::getCloseRelationshipName($head, $spouse_family_child) . ' - ' . $spouse_family_child->getFullName(),
476						'#',
477						'',
478						array('onclick' => 'return appendCensusRow("' . Filter::escapeJs(self::censusTableRow($census, $spouse_family_child, $head)) . '");')
479					);
480					$submenu->addClass('submenuitem', '');
481					$menu->addSubmenu($submenu);
482					$menu->addClass('', 'submenu');
483				}
484			}
485
486			?>
487			<tr>
488				<td class="optionbox">
489					<?php echo $menu->getMenu(); ?>
490				</td>
491				<td class="facts_value">
492					<a href="#" onclick="return appendCensusRow('<?php echo Filter::escapeJs(self::censusTableRow($census, $child, $head)); ?>');">
493						<?php echo $child->getFullName(); ?>
494					</a>
495				</td>
496				<td class="facts_value">
497					<a href="edit_interface.php?action=addnewnote_assisted&amp;noteid=newnote&amp;xref=<?php echo $child->getXref(); ?>&amp;gedcom=<?php echo $child->getTree()->getNameUrl(); ?>&amp;census=<?php echo get_class($census); ?>">
498						<?php echo $headImg2; ?>
499					</a>
500				</td>
501			</tr>
502			<?php
503		}
504		echo '<tr><td><br></td></tr>';
505	}
506}
507