<?php
// Base Report Generator
//
// used by the SAX parser to generate reports from the XML report file.
//
// webtrees: Web based Family History software
// Copyright (C) 2014 webtrees development team.
//
// Derived from PhpGedView
// Copyright (C) 2002 to 2009 PGV Development Team.  All rights reserved.
//
// 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 2 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, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

/**
 * Enable HTML code to pass for testing
 * false = use the old style, HTML disabled
 * true = use the new style, HTML enabled
 */
define("WT_RNEW", false);

/**
 * Main Report Class
 *
 * Document wide functions and variable defaults that will be inherited of the report modules
 */
class WT_Report_Base {
	/**
	 * Left Margin (expressed in points) Default: 17.99 mm, 0.7083 inch
	 *
	 * @see DocSHandler()
	 * @var float
	 */
	public $leftmargin = 51;
	/**
	 * Right Margin (expressed in points) Default: 9.87 mm, 0.389 inch
	 *
	 * @see DocSHandler()
	 * @var float
	 */
	public $rightmargin = 28;
	/**
	 * Top Margin (expressed in points) Default: 26.81 mm
	 *
	 * @see DocSHandler()
	 * @var float
	 */
	public $topmargin = 76;
	/**
	 * Bottom Margin (expressed in points) Default: 21.6 mm
	 *
	 * @see DocSHandler()
	 * @var float
	 */
	public $bottommargin = 60;
	/**
	 * Header Margin (expressed in points) Default: 4.93 mm
	 *
	 * @see DocSHandler()
	 * @var float
	 */
	public $headermargin = 14;
	/**
	 * Footer Margin (expressed in points) Default: 9.88 mm, 0.389 inch
	 *
	 * @see DocSHandler()
	 * @var float
	 */
	public $footermargin = 28;

	/**
	 * Page orientation (portrait, landscape)
	 *
	 * @see DocSHandler()
	 * @var string
	 */
	public $orientation = "portrait";
	/**
	 * Page format name
	 *
	 * @see DocSHandler()
	 * @see WT_Report_Base::setup()
	 * @var string
	 */
	public $pageFormat = "A4";

	/**
	 * Height of page format in points
	 *
	 * @see DocSHandler()
	 * @see WT_Report_Base::setup()
	 * @var float
	 */
	public $pageh = 0.0;
	/**
	 * Width of page format in points
	 *
	 * @see DocSHandler()
	 * @see WT_Report_Base::setup()
	 * @var float
	 */
	public $pagew = 0.0;

	/**
	 * An array of the Styles elements found in the document
	 *
	 * @see StyleSHandler()
	 * @var array
	 */
	public $Styles = array();
	/**
	 * The default Report font name
	 *
	 * @see StyleSHandler()
	 * @var string
	 */
	public $defaultFont = "dejavusans";
	/**
	 * The default Report font size
	 *
	 * @see StyleSHandler()
	 * @var int
	 */
	public $defaultFontSize = 12;

	/**
	 * Header (H), Page header (PH), Body (B) or Footer (F)
	 *
	 * @var string
	 */
	public $processing = "H";

	/**
	 * RTL Language (false=LTR, true=RTL)
	 *
	 * @see WT_Report_Base::setup()
	 * @var boolean
	 */
	public $rtl = false;

	/**
	 * User measure unit.
	 *
	 * @var string const
	 */
	const unit = "pt";

	/**
	 * Show the Generated by... (true=show the text)
	 *
	 * @see DocSHandler()
	 * @var boolean
	 */
	public $showGenText = true;
	/**
	 * Generated By... text
	 *
	 * @see WT_Report_Base::setup()
	 * @var string
	 */
	public $generatedby = "";

	/**
	 * webtrees URL
	 *
	 * @var string const
	 */
	const wt_url = WT_WEBTREES_URL;

	/**
	 * The report title
	 *
	 * @see WT_Report_Base::addTitle()
	 * @var string
	 */
	public $title = "";

	/**
	 * Author of the report, the users full name
	 *
	 * @var string
	 */
	public $rauthor = WT_SERVER_NAME;

	/**
	 * Keywords
	 *
	 * @see WT_Report_Base::setup()
	 * @var string
	 */
	public $rkeywords = "";

	/**
	 * Report Description / Subject
	 *
	 * @see WT_Report_Base::addDescription()
	 * @var string
	 */
	public $rsubject = "";

	/**
	 * Initial Setup - WT_Report_Base
	 *
	 * Setting up document wide defaults that will be inherited of the report modules
	 * As DEFAULT A4 and Portrait will be used if not set
	 *
	 * @see  DocSHandler()
	 */
	function setup() {
		global $TEXT_DIRECTION;

		// Set RTL direction
		if ($TEXT_DIRECTION == "rtl") {
			$this->rtl = true;
		}
		// Set the Keywords
		$this->rkeywords = '';
		// Generated By...text
		// I18N: This is a report footer. %s is the name of the application.
		$this->generatedby = WT_I18N::translate('Generated by %s', WT_WEBTREES . ' ' . WT_VERSION);

		// For known size pages
		if (($this->pagew == 0) AND ($this->pageh == 0)) {
			/**
			 * The current ISO 216 standard was introduced in 1975 and is a direct follow up to the german DIN 476 standard from 1922. ISO 216 is also called EN 20216 in Europe.
			 * The ISO paper sizes are based on the metric system so everything else is aproxiamte
			 *
			 * The Series A is used for Standard Printing and Stationary.
			 * The Series B is used for Posters, Wall-Charts etc.
			 * The C series is used for folders, post cards and envelopes. C series envelope is suitable to insert A series sizes.
			 * ISO also define format series RA and SRA for untrimmed raw paper, where SRA stands for 'supplementary raw format A'.
			 * Japan has adopted the ISO series A sizes, but its series B sizes are slightly different. These sizes are sometimes called JIS B or JB sizes.
			 *  sun was a unit of length used in Japan and is equal to about 3.03 cm or 1.193 inches
			 * The United States, Canada, and in part Mexico, are today the only industrialized nations in which the ISO standard paper sizes are not yet widely used.
			 *
			 * A0 & A1        Technical drawings, posters
			 * A1 & A2        Flip charts
			 * A2 & A3        Drawings, diagrams, large tables
			 * A4             Letters, magazines, forms, catalogs, laser printer and copying machine output
			 * A5             Note pads
			 * A6             Postcards
			 * B5, A5, B6  A6 Books
			 * C4, C5, C6     Envelopes for A4 letters: unfolded (C4), folded once (C5), folded twice (C6)
			 * B4 & A3        Newspapers, supported by most copying machines in addition to A4
			 * B8 & A8        Playing cards
			 *
			 * 1 inch = 72 points
			 * 1 mm = 2.8346457 points
			 * 1 inch = 25.4 mm
			 * 1 point = 0,35278 mm
			 */
			switch ($this->pageFormat) {
				// ISO A series
			case "4A0":
			{
				$sizes = array(4767.86, 6740.79);
				break;
			} // ISO 216, 1682 mm x 2378 mm
			case "2A0":
			{
				$sizes = array(3370.39, 4767.86);
				break;
			} // ISO 216, 1189 mm x 1682 mm
			case "A0":
			{
				$sizes = array(2383.94, 3370.39);
				break;
			} // ISO 216, 841 mm x 1189mm
			case "A1":
			{
				$sizes = array(1683.78, 2383.94);
				break;
			} // ISO 216, 594 mm x 841 mm
			case "A2":
			{
				$sizes = array(1190.55, 1683.78);
				break;
			} // ISO 216, 420 mm x 594 mm
			case "A3":
			{
				$sizes = array(841.89, 1190.55);
				break;
			} // ISO 216, 297 mm x 420 mm
			case "A4":
			default:
				{ // For unknown Page Size Name
				$sizes            = array(595.28, 841.89); // ISO 216, 210 mm 297 mm
				$this->pageFormat = "A4";
				break;
				}
			case "A5":
			{
				$sizes = array(419.53, 595.28);
				break;
			} // ISO 216, 148 mm x 210 mm
			case "A6":
			{
				$sizes = array(297.64, 419.53);
				break;
			} // ISO 216, 105 mm x 148 mm
			case "A7":
			{
				$sizes = array(209.76, 297.64);
				break;
			} // ISO 216, 74 mm x 105 mm
			case "A8":
			{
				$sizes = array(147.40, 209.76);
				break;
			} // ISO 216, 52 mm x 74 mm
			case "A9":
			{
				$sizes = array(104.88, 147.40);
				break;
			} // ISO 216, 37 mm x 52 mm
			case "A10":
			{
				$sizes = array(73.70, 104.88);
				break;
			} // ISO 216, 26 mm x 37 mm
				// ISO B series
			case "B0":
			{
				$sizes = array(2834.65, 4008.19);
				break;
			} // ISO 216, 1000 mm x 1414 mm
			case "B1":
			{
				$sizes = array(2004.09, 2834.65);
				break;
			} // ISO 216, 707 mm x 1000 mm
			case "B2":
			{
				$sizes = array(1417.32, 2004.09);
				break;
			} // ISO 216, 500 mm x 707 mm
			case "B3":
			{
				$sizes = array(1000.63, 1417.32);
				break;
			} // ISO 216, 353 mm x 500 mm
			case "B4":
			{
				$sizes = array(708.66, 1000.63);
				break;
			} // ISO 216, 250 mm x 353 mm
			case "B5":
			{
				$sizes = array(498.90, 708.66);
				break;
			} // ISO 216, 176 mm x 250 mm
			case "B6":
			{
				$sizes = array(354.33, 498.90);
				break;
			} // ISO 216, 125 mm x 176 mm
			case "B7":
			{
				$sizes = array(249.45, 354.33);
				break;
			} // ISO 216, 88 mm x 125 mm
			case "B8":
			{
				$sizes = array(175.75, 249.45);
				break;
			} // ISO 216, 62 mm x 88 mm
			case "B9":
			{
				$sizes = array(124.72, 175.75);
				break;
			} // ISO 216, 44 mm x 62 mm
			case "B10":
			{
				$sizes = array(87.87, 124.72);
				break;
			} // ISO 216, 31 mm x 44 mm
				// ISO C series, Envelope
			case "C0":
			{
				$sizes = array(2599.37, 3676.54);
				break;
			} // ISO 269, 917 mm x 1297 mm, For flat A0 sheet
			case "C1":
			{
				$sizes = array(1836.85, 2599.37);
				break;
			} // ISO 269, 648 mm x 917 mm, For flat A1 sheet
			case "C2":
			{
				$sizes = array(1298.27, 1836.85);
				break;
			} // ISO 269, 458 mm x 648 mm, For flat A2 sheet, A1 folded in half
			case "C3":
			{
				$sizes = array(918.43, 1298.27);
				break;
			} // ISO 269, 324 mm x 458 mm, For flat A3 sheet, A2 folded in half
			case "C4":
			{
				$sizes = array(649.13, 918.43);
				break;
			} // ISO 269, 229 mm x 324 mm, For flat A4 sheet, A3 folded in half
			case "C5":
			{
				$sizes = array(459.21, 649.13);
				break;
			} // ISO 269, 162 mm x 229 mm, For flat A5 sheet, A4 folded in half
			case "C6/5":
			{
				$sizes = array(323.15, 649.13);
				break;
			} // ISO 269, 114 mm x 229 mm. A5 folded twice = 1/3 A4. Alternative for the DL envelope
			case "C6":
			{
				$sizes = array(323.15, 459.21);
				break;
			} // ISO 269, 114 mm x 162 mm, For A5 folded in half
			case "C7/6":
			{
				$sizes = array(229.61, 459.21);
				break;
			} // ISO 269, 81 mm x 162 mm, For A5 sheet folded in thirds
			case "C7":
			{
				$sizes = array(229.61, 323.15);
				break;
			} // ISO 269, 81 mm x 114 mm, For A5 folded in quarters
			case "C8":
			{
				$sizes = array(161.57, 229.61);
				break;
			} // ISO 269, 57 mm x 81 mm
			case "C9":
			{
				$sizes = array(113.39, 161.57);
				break;
			} // ISO 269, 40 mm x 57 mm
			case "C10":
			{
				$sizes = array(79.37, 113.39);
				break;
			} // ISO 269, 28 mm x 40 mm
			case "DL":
			{
				$sizes = array(311.81, 623.62);
				break;
			} // Original DIN 678 but ISO 269 now has this C6/5 , 110 mm x 220 mm, For A4 sheet folded in thirds, A5 in half
				// Untrimmed stock sizes for the ISO-A Series - ISO primary range
			case "RA0":
			{
				$sizes = array(2437.80, 3458.27);
				break;
			} // ISO 478, 860 mm x 1220 mm
			case "RA1":
			{
				$sizes = array(1729.13, 2437.80);
				break;
			} // ISO 478, 610 mm x 860 mm
			case "RA2":
			{
				$sizes = array(1218.90, 1729.13);
				break;
			} // ISO 478, 430 mm x 610 mm
			case "RA3":
			{
				$sizes = array(864.57, 1218.90);
				break;
			} // ISO 478, 305 mm x 430 mm
			case "RA4":
			{
				$sizes = array(609.45, 864.57);
				break;
			} // ISO 478, 215 mm x 305 mm
				// Untrimmed stock sizes for the ISO-A Series - ISO supplementary range
			case "SRA0":
			{
				$sizes = array(2551.18, 3628.35);
				break;
			} // ISO 593, 900 mm x 1280 mm
			case "SRA1":
			{
				$sizes = array(1814.17, 2551.18);
				break;
			} // ISO 593, 640 mm x 900 mm
			case "SRA2":
			{
				$sizes = array(1275.59, 1814.17);
				break;
			} // ISO 593, 450 mm x 640 mm
			case "SRA3":
			{
				$sizes = array(907.09, 1275.59);
				break;
			} // ISO 593, 320 mm x 450 mm
			case "SRA4":
			{
				$sizes = array(637.80, 907.09);
				break;
			} // ISO 593, 225 mm x 320 mm
				// ISO size variations
			case "A2EXTRA":
			{
				$sizes = array(1261.42, 1754.65);
				break;
			} // ISO 216, 445 mm x 619 mm
			case "A2SUPER":
			{
				$sizes = array(864.57, 1440.00);
				break;
			} // ISO 216, 305 mm x 508 mm
			case "A3EXTRA":
			{
				$sizes = array(912.76, 1261.42);
				break;
			} // ISO 216, 322 mm x 445 mm
			case "SUPERA3":
			{
				$sizes = array(864.57, 1380.47);
				break;
			} // ISO 216, 305 mm x 487 mm
			case "A4EXTRA":
			{
				$sizes = array(666.14, 912.76);
				break;
			} // ISO 216, 235 mm x 322 mm
			case "A4LONG":
			{
				$sizes = array(595.28, 986.46);
				break;
			} // ISO 216, 210 mm x 348 mm
			case "A4SUPER":
			{
				$sizes = array(649.13, 912.76);
				break;
			} // ISO 216, 229 mm x 322 mm
			case "SUPERA4":
			{
				$sizes = array(643.46, 1009.13);
				break;
			} // ISO 216, 227 mm x 356 mm
			case "A5EXTRA":
			{
				$sizes = array(490.39, 666.14);
				break;
			} // ISO 216, 173 mm x 235 mm
			case "SOB5EXTRA":
			{
				$sizes = array(572.60, 782.36);
				break;
			} // ISO 216, 202 mm x 276 mm
				// Japanese version of the ISO 216 B series
			case "JB0":
			{
				$sizes = array(2919.69, 4127.24);
				break;
			} // JIS P 0138-61, 1030 mm x 1456 mm
			case "JB1":
			{
				$sizes = array(2063.62, 2919.69);
				break;
			} // JIS P 0138-61, 728 mm x 1030 mm
			case "JB2":
			{
				$sizes = array(1459.84, 2063.62);
				break;
			} // JIS P 0138-61, 515 mm x 728 mm
			case "JB3":
			{
				$sizes = array(1031.81, 1459.84);
				break;
			} // JIS P 0138-61, 364 mm x 515 mm
			case "JB4":
			{
				$sizes = array(728.50, 1031.81);
				break;
			} // JIS P 0138-61, 257 mm x 364 mm
			case "JB5":
			{
				$sizes = array(515.91, 728.50);
				break;
			} // JIS P 0138-61, 182 mm x 257 mm
			case "JB6":
			{
				$sizes = array(362.83, 515.91);
				break;
			} // JIS P 0138-61, 128 mm x 182 mm
			case "JB7":
			{
				$sizes = array(257.95, 362.83);
				break;
			} // JIS P 0138-61, 91 mm x 128 mm
			case "JB8":
			{
				$sizes = array(181.42, 257.95);
				break;
			} // JIS P 0138-61, 64 mm x 91 mm
			case "JB9":
			{
				$sizes = array(127.56, 181.42);
				break;
			} // JIS P 0138-61, 45 mm x 64 mm
			case "JB10":
			{
				$sizes = array(90.71, 127.56);
				break;
			} // JIS P 0138-61, 32 mm x 45 mm
				// US pages
			case "EXECUTIVE":
			{
				$sizes = array(522.00, 756.00);
				break;
			} // 7.25 in x 10.5 in
			case "FOLIO":
			{
				$sizes = array(612.00, 936.00);
				break;
			} // 8.5 in x 13 in
			case "FOOLSCAP":
			{
				$sizes = array(972.00, 1224.00);
				break;
			} // 13.5 in x 17 in
			case "LEDGER":
			{
				$sizes = array(792.00, 1224.00);
				break;
			} // 11 in x 17 in
			case "LEGAL":
			{
				$sizes = array(612.00, 1008.00);
				break;
			} // 8.5 in x 14 in
			case "LETTER":
			{
				$sizes = array(612.00, 792.00);
				break;
			} // 8.5 in x 11 in
			case "QUARTO":
			{
				$sizes = array(609.12, 777.50);
				break;
			} // 8.46 in x 10.8 in
			case "STATEMENT":
			{
				$sizes = array(396.00, 612.00);
				break;
			} // 5.5 in x 8.5 in
			case "USGOVT":
			{
				$sizes = array(576.00, 792.00);
				break;
			} // 8 in x 11 in
			}
			$this->pagew = $sizes[0];
			$this->pageh = $sizes[1];
		} else {
			if ($this->pagew < 10) {
				die("<strong>REPORT ERROR WT_Report_Base::setup(): </strong>For custom size pages you must set \"customwidth\" larger then this in the XML file");
			}
			if ($this->pageh < 10) {
				die("<strong>REPORT ERROR WT_Report_Base::setup(): </strong>For custom size pages you must set \"customheight\" larger then this in the XML file");
			}
		}

		return 0;
	}

	/**
	 * Process the Header , Page header, Body or Footer - WT_Report_Base
	 *
	 * @param string $p Header (H), Page header (PH), Body (B) or Footer (F)
	 *
	 * @return int
	 */
	function setProcessing($p) {
		$this->processing = $p;

		return 0;
	}

	/**
	 * Add the Title when raw character data is used in Title - WT_Report_Base
	 *
	 * @param string $data
	 *
	 * @return int
	 */
	function addTitle($data) {
		$this->title .= $data;

		return 0;
	}

	/**
	 * Add the Description when raw character data is used in Description - WT_Report_Base
	 *
	 * @param string $data
	 *
	 * @return int
	 */
	function addDescription($data) {
		$this->rsubject .= $data;

		return 0;
	}

	/**
	 * Add Style to Styles array - WT_Report_Base
	 *
	 * @see StyleSHandler()
	 *
	 * @param array $style
	 *
	 * @return int
	 */
	function addStyle($style) {
		$this->Styles[$style['name']] = $style;

		return 0;
	}

	/**
	 * Get a style from the Styles array - WT_Report_Base
	 *
	 * @param string $s Style name
	 *
	 * @return array
	 */
	function getStyle($s) {
		if (!isset($this->Styles[$s])) {
			return current($this->Styles);
		}

		return $this->Styles[$s];
	}
}

/**
 * Main WT Report Element class that all other page elements are extended from
 */
class Element {
	/**
	 * @var string
	 */
	public $text = "";

	/**
	 * Element renderer
	 *
	 * @param $renderer
	 *
	 * @return int
	 */
	function render($renderer) {
		//print "Nothing rendered.  Something bad happened";
		//debug_print_backtrace();
		//-- to be implemented in inherited classes
		return 0;
	}

	function getHeight($renderer) {
		return 0;
	}

	function getWidth($renderer) {
		return 0;
	}

	function addText($t) {
		global $wt_report, $reportTitle, $reportDescription;

		$t = trim($t, "\r\n\t");
		$t = str_replace(array("<br>", "&nbsp;"), array("\n", " "), $t);
		if (!WT_RNEW) {
			$t = strip_tags($t);
			$t = htmlspecialchars_decode($t);
		}
		$this->text .= $t;

		// Adding the title and description to the Document Properties
		if ($reportTitle) {
			$wt_report->addTitle($t);
		} elseif ($reportDescription) {
			$wt_report->addDescription($t);
		}

		return 0;
	}

	function addNewline() {
		$this->text .= "\n";

		return 0;
	}

	function getValue() {
		return $this->text;
	}

	function setWrapWidth($wrapwidth, $cellwidth) {
		return 0;
	}

	function renderFootnote($renderer) {
		return false;
		//-- to be implemented in inherited classes
	}

	/**
	 * Get the Class name type
	 *
	 * @return string ElementBase
	 */
	function get_type() {
		return "ElementBase";
	}

	function setText($text) {
		$this->text = $text;

		return 0;
	}
} //-- END Element

/**
 * HTML element class
 */
class Html extends Element {
	public $tag;
	public $attrs;
	public $elements = array();

	function Html($tag, $attrs) {
		$this->tag   = $tag;
		$this->attrs = $attrs;

		return 0;
	}

	function getStart() {
		$str = "<" . $this->tag . " ";
		foreach ($this->attrs as $key => $value) {
			$str .= $key . "=\"" . $value . "\" ";
		}
		$str .= ">";

		return $str;
	}

	function getEnd() {
		return "</" . $this->tag . ">";
	}

	function addElement($element) {
		$this->elements[] = $element;

		return 0;
	}

	/**
	 * Get the class type
	 *
	 * @return string Html
	 */
	function get_type() {
		return "Html";
	}
}

/**
 * Cell element class
 */
class Cell extends Element {
	/**
	 * Allows to center or align the text. Possible values are:<ul><li>left or empty string: left align</li><li>center: center align</li><li>right: right align</li><li>justify: justification (default value when $ishtml=false)</li></ul>
	 *
	 * @var string
	 */
	public $align = "";
	/**
	 * Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0.
	 * Or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
	 *
	 * @var mixed
	 */
	public $border;
	/**
	 * Border color in HTML code
	 *
	 * @var string
	 */
	public $bocolor;
	/**
	 * The HTML color code to fill the background of this cell.
	 *
	 * @var string
	 */
	public $bgcolor;
	/**
	 * Indicates if the cell background must be painted (1) or transparent (0). Default value: 1.
	 * If no background color is set then it will not be painted
	 *
	 * @var int
	 */
	public $fill;
	/**
	 * Cell height DEFAULT 0 (expressed in points)
	 * The starting height of this cell. If the text wraps the height will automatically be adjusted.
	 *
	 * @var int
	 */
	public $height;
	/**
	 * Left position in user units (X-position). Default is the current position
	 *
	 * @var mixed
	 */
	public $left;
	/**
	 * Indicates where the current position should go after the call.  Possible values are:<ul><li>0: to the right [DEFAULT]</li><li>1: to the beginning of the next line</li><li>2: below</li></ul>
	 *
	 * @var int
	 */
	public $newline;
	/**
	 * The name of the Style that should be used to render the text.
	 *
	 * @var string
	 */
	public $styleName;
	/**
	 * Stretch carachter mode: <ul><li>0 = disabled (default)</li><li>1 = horizontal scaling only if necessary</li><li>2 = forced horizontal scaling</li><li>3 = character spacing only if necessary</li><li>4 = forced character spacing</li></ul>
	 *
	 * @var int
	 */
	public $stretch;
	/**
	 * Text color in HTML code
	 *
	 * @var string
	 */
	public $tcolor;
	/**
	 * Top position in user units (Y-position). Default is the current position
	 *
	 * @var mixed
	 */
	public $top;
	/**
	 * URL address
	 *
	 * @var string
	 */
	public $url;
	/**
	 * Cell width DEFAULT 0 (expressed in points)
	 * Setting the width to 0 will make it the width from the current location to the right margin.
	 *
	 * @var int
	 */
	public $width;

	public $reseth;

	/**
	 * CELL - Element
	 *
	 * @param int    $width   cell width (expressed in points)
	 * @param int    $height  cell height (expressed in points)
	 * @param mixed  $border  Border style
	 * @param string $align   Text alignement
	 * @param string $bgcolor Background color code
	 * @param string $style   The name of the text style
	 * @param int    $ln      Indicates where the current position should go after the call
	 * @param mixed  $top     Y-position
	 * @param mixed  $left    X-position
	 * @param int    $fill    Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
	 * @param int    $stretch Stretch carachter mode
	 * @param string $bocolor Border color
	 * @param string $tcolor  Text color
	 * @param        $reseth
	 *
	 * @return int
	 */
	function Cell(
		$width, $height, $border, $align, $bgcolor, $style, $ln, $top, $left, $fill, $stretch, $bocolor, $tcolor, $reseth
	) {
		$this->align     = $align;
		$this->border    = $border;
		$this->bgcolor   = $bgcolor;
		$this->bocolor   = $bocolor;
		$this->fill      = $fill;
		$this->height    = $height;
		$this->left      = $left;
		$this->newline   = $ln;
		$this->styleName = $style;
		$this->text      = "";
		$this->tcolor    = $tcolor;
		$this->top       = $top;
		$this->url       = "";
		$this->stretch   = $stretch;
		$this->width     = $width;
		$this->reseth    = $reseth;

		return 0;
	}

	/**
	 * Get the cell height
	 *
	 * @param $renderer
	 *
	 * @return float
	 */
	function getHeight($renderer) {
		return $this->height;
	}

	/**
	 * Sets the current cells URL
	 *
	 * @param string $url The URL address to save
	 *
	 * @return int
	 */
	function setUrl($url) {
		$this->url = $url;

		return 0;
	}

	/**
	 * Get the cell width
	 *
	 * @param $renderer
	 *
	 * @return float
	 */
	function getWidth($renderer) {
		return $this->width;
	}

	/**
	 * Get the class type
	 *
	 * @return string Cell
	 */
	function get_type() {
		return "Cell";
	}
}

/**
 * TextBox element class
 *
 */
class TextBox extends Element {
	/**
	 * Array of elements in the TextBox
	 *
	 * @var array
	 */
	public $elements = array();

	/**
	 *  Background color in HTML code
	 *
	 * @var string
	 */
	public $bgcolor;
	/**
	 * Whether or not paint the background
	 *
	 * @var boolean
	 */
	public $fill;

	/**
	 * Position the left corner of this box on the page(expressed in points). The default is the current position.
	 *
	 * @var mixed
	 */
	public $left;
	/**
	 * Position the top corner of this box on the page(expressed in points). the default is the current position
	 *
	 * @var mixed
	 */
	public $top;
	/**
	 * After this box is finished rendering, should the next section of text start immediately after the this box or should it start on a new line under this box. 0 = no new line, 1 = force new line. Default is 0
	 *
	 * @var boolean
	 */
	public $newline;

	/**
	 * @var boolean
	 */
	public $pagecheck;

	/**
	 * Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
	 *
	 * @var boolean
	 */
	public $border;
	/**
	 * Style of rendering
	 *
	 * <ul>
	 * <li>D or empty string: Draw (default).</li>
	 * <li>F: Fill.</li>
	 * <li>DF or FD: Draw and fill.</li>
	 * <li>CNZ: Clipping mode (using the even-odd rule to determine which regions lie inside the clipping path).</li>
	 *<li>CEO: Clipping mode (using the nonzero winding number rule to determine which regions lie inside the clipping path).</li>
	 * </ul>
	 *
	 * @var string
	 */
	public $style;

	/**
	 * @var array $borderstyle Border style of rectangle. Array with keys among the following:
	 * <ul>
	 * <li>all: Line style of all borders. Array like for {@link SetLineStyle SetLineStyle}.</li>
	 * <li>L, T, R, B or combinations: Line style of left, top, right or bottom border. Array like for {@link SetLineStyle SetLineStyle}.</li>
	 * </ul>
	 * Not yet in use
	 * var $borderstyle;
	 */

	/**
	 * The starting height of this cell. If the text wraps the height will automatically be adjusted
	 *
	 * @var float
	 */
	public $height;
	/**
	 * Setting the width to 0 will make it the width from the current location to the right margin
	 *
	 * @var float
	 */
	public $width;
	/**
	 * Use cell padding or not
	 *
	 * @var boolean $padding
	 */
	public $padding;
	/**
	 * Resets this box last height after it’s done
	 */
	public $reseth;

	/**
	 * TextBox - Element - Base
	 *
	 * @param float   $width   Text box width
	 * @param float   $height  Text box height
	 * @param boolean $border
	 * @param string  $bgcolor Background color code in HTML
	 * @param boolean $newline
	 * @param mixed   $left
	 * @param mixed   $top
	 * @param boolean $pagecheck
	 * @param string  $style
	 * @param boolean $fill
	 * @param boolean $padding
	 * @param boolean $reseth
	 *
	 * @return int
	 */
	function TextBox(
		$width, $height, $border, $bgcolor, $newline, $left, $top, $pagecheck, $style, $fill, $padding, $reseth
	) {
		$this->border    = $border;
		$this->bgcolor   = $bgcolor;
		$this->fill      = $fill;
		$this->height    = $height;
		$this->left      = $left;
		$this->newline   = $newline;
		$this->pagecheck = $pagecheck;
		$this->style     = $style;
		$this->top       = $top;
		$this->width     = $width;
		$this->padding   = $padding;
		$this->reseth    = $reseth;

		return 0;
	}

	/**
	 * Add an element to the TextBox
	 *
	 * @param object|string $element
	 *
	 * @return int
	 */
	function addElement($element) {
		$this->elements[] = $element;

		return 0;
	}

	/**
	 * Get the class type
	 *
	 * @return string TextBox
	 */
	function get_type() {
		return "TextBox";
	}
}

/**
 * Text element class
 */
class Text extends Element {
	/**
	 * Text color in HTML code
	 *
	 * @var string
	 */
	public $color;
	/**
	 * Style name
	 *
	 * @var string
	 */
	public $styleName;
	/**
	 * Remaining width of a cel
	 *
	 * @var int User unit (points)
	 */
	public $wrapWidthRemaining;
	/**
	 * Original width of a cell
	 *
	 * @var int User unit (points)
	 */
	public $wrapWidthCell;

	/**
	 * Create a Text class - Base
	 *
	 * @param string $style The name of the text style
	 * @param string $color HTML color code
	 *
	 * @return int
	 */
	function Text($style, $color) {
		$this->text               = "";
		$this->color              = $color;
		$this->wrapWidthRemaining = 0;
		$this->styleName          = $style;

		return 0;
	}

	function setWrapWidth($wrapwidth, $cellwidth) {
		$this->wrapWidthCell = $cellwidth;
		if (strpos($this->text, "\n") !== false) {
			$this->wrapWidthRemaining = $cellwidth;
		} else {
			$this->wrapWidthRemaining = $wrapwidth;
		}

		return $this->wrapWidthRemaining;
	}

	function getStyleName() {
		return $this->styleName;
	}

	/**
	 * Get the class type
	 *
	 * @return string Text
	 */
	function get_type() {
		return "Text";
	}
}

/**
 * Footnote element class
 *
 */
class Footnote extends Element {
	/**
	 * The name of the style for this element
	 *
	 * @var string
	 */
	public $styleName = "";
	/**
	 * Numbers for the links
	 *
	 * @var int
	 */
	public $num;
	/**
	 * The text that will be printed with the number
	 *
	 * @var string
	 */
	public $numText = "";
	/**
	 * Remaining width of a cell
	 *
	 * @var float User unit (points)
	 */
	public $wrapWidthRemaining;
	/**
	 * Original width of a cell
	 *
	 * @var float User unit (points)
	 */
	public $wrapWidthCell;
	public $addlink;

	function Footnote($style = "") {
		$this->text = "";
		if (!empty($style)) {
			$this->styleName = $style;
		} else {
			$this->styleName = "footnote";
		}

		return 0;
	}

	function rerender($renderer) {
		return false;
	}

	function addText($t) {
		$t = trim($t, "\r\n\t");
		$t = str_replace(array("<br>", "&nbsp;"), array("\n", " "), $t);
		if (!WT_RNEW) {
			$t = strip_tags($t);
			$t = htmlspecialchars_decode($t);
		}
		$this->text .= $t;

		return 0;
	}

	function setWrapWidth($wrapwidth, $cellwidth) {
		$this->wrapWidthCell = $cellwidth;
		if (strpos($this->numText, "\n") !== false) {
			$this->wrapWidthRemaining = $cellwidth;
		} else {
			$this->wrapWidthRemaining = $wrapwidth;
		}

		return $this->wrapWidthRemaining;
	}

	function setNum($n) {
		$this->num     = $n;
		$this->numText = "$n ";

		return 0;
	}

	function setAddlink($a) {
		$this->addlink = $a;

		return 0;
	}

	/**
	 * Get the class type
	 *
	 * @return string Footnote
	 */
	function get_type() {
		return "Footnote";
	}
}

/**
 * PageHeader element class
 */
class PageHeader extends Element {
	public $elements = array();

	function TextBox() {
		$this->elements = array();

		return 0;
	}

	function PageHeader() {
		$this->elements = array();

		return 0;
	}

	/**
	 * Add element - PageHeader
	 *
	 * @param $element
	 *
	 * @return int
	 */
	function addElement($element) {
		$this->elements[] = $element;

		return 0;
	}

	/**
	 * Get the class type
	 *
	 * @return string PageHeader
	 */
	function get_type() {
		return "PageHeader";
	}
}

/**
 * Image element class
 */
class Image extends Element {
	/**
	 * Filename of the image
	 *
	 * @var string
	 */
	public $file;
	/**
	 * Height of the image
	 *
	 * @var float
	 */
	public $height;
	/**
	 * Width of the image
	 *
	 * @var float
	 */
	public $width;
	/**
	 * X-position (left) of the image
	 *
	 * @var float
	 */
	public $x;
	/**
	 * Y-position (top) of the image
	 *
	 * @var float
	 */
	public $y;
	/**
	 * Placement fo the image. L: left, C:center, R:right
	 *
	 * @var string
	 */
	public $align = "";
	/**
	 * T:same line, N:next line
	 *
	 * @var string
	 */
	public $line = "";

	/**
	 * Image class function - Base
	 *
	 * @param string $file  Filename of the image
	 * @param float  $x     X-position (left) of the image
	 * @param float  $y     Y-position (top) of the image
	 * @param float  $w     Width of the image
	 * @param float  $h     Height of the image
	 * @param string $align Placement of the image. L: left, C:center, R:right
	 * @param string $ln    T:same line, N:next line
	 *
	 * @return int
	 */
	function Image($file, $x, $y, $w, $h, $align, $ln) {
		$this->file   = $file;
		$this->width  = $w;
		$this->height = $h;
		$this->x      = $x;
		$this->y      = $y;
		$this->align  = $align;
		$this->line   = $ln;

		return 0;
	}

	function getHeight($renderer) {
		return $this->height;
	}

	function getWidth($renderer) {
		return $this->width;
	}

	/**
	 * Get the class type
	 *
	 * @return string Image
	 */
	function get_type() {
		return "Image";
	}
}

/**
 * Line element class
 */
class Line extends Element {
	/**
	 * Start horizontal position, current position (default)
	 *
	 * @var mixed
	 */
	public $x1 = ".";
	/**
	 * Start vertical position, current position (default)
	 *
	 * @var mixed
	 */
	public $y1 = ".";
	/**
	 * End horizontal position, maximum width (default)
	 *
	 * @var mixed
	 */
	public $x2 = ".";
	/**
	 * End vertical position
	 *
	 * @var mixed
	 */
	public $y2 = ".";

	/**
	 * Create a line class - Base
	 *
	 * @param mixed $x1
	 * @param mixed $y1
	 * @param mixed $x2
	 * @param mixed $y2
	 *
	 * @return int
	 */
	function Line($x1, $y1, $x2, $y2) {
		$this->x1 = $x1;
		$this->y1 = $y1;
		$this->x2 = $x2;
		$this->y2 = $y2;

		return 0;
	}

	function getHeight($renderer) {
		return abs($this->y2 - $this->y1);
	}

	function getWidth($renderer) {
		return abs($this->x2 - $this->x1);
	}

	/**
	 * Get the class type
	 *
	 * @return string Line
	 */
	function get_type() {
		return "Line";
	}
}

/**
 *XML start element handler
 *
 * This function is called whenever a starting element is reached
 * The element handler will be called if found, otherwise it must be HTML
 *
 * @param resource $parser the resource handler for the XML parser
 * @param string   $name   the name of the XML element parsed
 * @param array    $attrs  an array of key value pairs for the attributes
 *
 * @see endElement()
 */
function startElement($parser, $name, $attrs) {
	global $elementHandler, $processIfs, $processGedcoms, $processRepeats, $vars;
	global $processFootnote;

	$newattrs = array();
	$match    = array();

	foreach ($attrs as $key => $value) {
		if (preg_match("/^\\$(\w+)$/", $value, $match)) {
			if ((isset($vars[$match[1]]['id'])) && (!isset($vars[$match[1]]['gedcom']))) {
				$value = $vars[$match[1]]['id'];
			}
		}
		$newattrs[$key] = $value;
	}
	$attrs = $newattrs;
	if (($processFootnote) && ($processIfs == 0 || $name == "if") && ($processGedcoms == 0 || $name == "Gedcom") && ($processRepeats == 0 || $name == "Facts" || $name == "RepeatTag")) {
		if (isset($elementHandler[$name]['start'])) {
			if ($elementHandler[$name]['start'] != "") {
				call_user_func($elementHandler[$name]['start'], $attrs);
			}
		} elseif (!isset($elementHandler[$name]['end'])) {
			HTMLSHandler($name, $attrs);
		}
	}
}

/**
 * XML end element handler
 *
 * This function is called whenever an ending element is reached
 * The element handler will be called if found, otherwise it must be HTML
 *
 * @param resource $parser the resource handler for the XML parser
 * @param string   $name   the name of the XML element parsed
 *
 * @see startElement()
 */
function endElement($parser, $name) {
	global $elementHandler, $processIfs, $processGedcoms, $processRepeats;
	global $processFootnote;

	if (($processFootnote || $name == "Footnote") && ($processIfs == 0 || $name == "if") && ($processGedcoms == 0 || $name == "Gedcom") && ($processRepeats == 0 || $name == "Facts" || $name == "RepeatTag" || $name == "List" || $name == "Relatives")) {
		if (isset($elementHandler[$name]['end'])) {
			if ($elementHandler[$name]['end'] != "") {
				call_user_func($elementHandler[$name]['end']);
			}
		} elseif (!isset($elementHandler[$name]['start'])) {
			HTMLEHandler($name);
		}
	}
}

/**
 * XML character data handler
 *
 * This function is called whenever raw character data is reached
 * just print it to the screen
 *
 * @param resource $parser the resource handler for the XML parser
 * @param string   $data   the name of the XML element parsed
 */
function characterData($parser, $data) {
	global $printData, $currentElement, $processGedcoms, $processIfs, $processRepeats, $reportTitle, $wt_report, $reportDescription;

	if ($printData && ($processGedcoms == 0) && ($processIfs == 0) && ($processRepeats == 0)) {
		$currentElement->addText($data);
	} elseif ($reportTitle) {
		$wt_report->addTitle($data);
	} elseif ($reportDescription) {
		$wt_report->addDescription($data);
	}
}

/**
 * XML <StyleSHandler /> element handler
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see  WT_Report_Base::$defaultFont
 * @see  WT_Report_Base::$defaultFontSize
 * @see  WT_Report_Base::addStyle()
 */
function StyleSHandler($attrs) {
	global $wt_report;

	if (empty($attrs['name'])) {
		die("<strong>REPORT ERROR Style: </strong> The \"name\" of the style is missing or not set in the XML file.");
	}

	// array Style that will be passed on
	$s = array();

	// string Name af the style
	$s['name'] = $attrs['name'];

	// string Name of the DEFAULT font
	$s['font'] = $wt_report->defaultFont;
	if (!empty($attrs['font'])) {
		$s['font'] = $attrs['font'];
	}

	// int The size of the font in points
	$s['size'] = $wt_report->defaultFontSize;
	if (!empty($attrs['size'])) {
		$s['size'] = (int) $attrs['size'];
	} // Get it as int to ignore all decimal points or text (if any text then int(0))

	// string B: bold, I: italic, U: underline, D: line trough, The default value is regular.
	$s['style'] = "";
	if (!empty($attrs['style'])) {
		$s['style'] = $attrs['style'];
	}

	$wt_report->addStyle($s);
}

/**
 * XML <Doc> start element handler
 *
 * Sets up the basics of the document proparties
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see  DocEHandler()
 * @see  WT_Report_Base::setup()
 */
function DocSHandler($attrs) {
	global $parser, $xml_parser, $wt_report;

	$parser = $xml_parser;

	// Custom page width
	if (!empty($attrs['customwidth'])) {
		$wt_report->pagew = (int) $attrs['customwidth'];
	} // Get it as int to ignore all decimal points or text (if any text then int(0))
	// Custom Page height
	if (!empty($attrs['customheight'])) {
		$wt_report->pageh = (int) $attrs['customheight'];
	} // Get it as int to ignore all decimal points or text (if any text then int(0))

	// Left Margin
	if (isset($attrs['leftmargin'])) {
		if ($attrs['leftmargin'] === "0") {
			$wt_report->leftmargin = 0;
		} elseif (!empty($attrs['leftmargin'])) {
			$wt_report->leftmargin = (int) $attrs['leftmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
		}
	}
	// Right Margin
	if (isset($attrs['rightmargin'])) {
		if ($attrs['rightmargin'] === "0") {
			$wt_report->rightmargin = 0;
		} elseif (!empty($attrs['rightmargin'])) {
			$wt_report->rightmargin = (int) $attrs['rightmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
		}
	}
	// Top Margin
	if (isset($attrs['topmargin'])) {
		if ($attrs['topmargin'] === "0") {
			$wt_report->topmargin = 0;
		} elseif (!empty($attrs['topmargin'])) {
			$wt_report->topmargin = (int) $attrs['topmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
		}
	}
	// Bottom Margin
	if (isset($attrs['bottommargin'])) {
		if ($attrs['bottommargin'] === "0") {
			$wt_report->bottommargin = 0;
		} elseif (!empty($attrs['bottommargin'])) {
			$wt_report->bottommargin = (int) $attrs['bottommargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
		}
	}
	// Header Margin
	if (isset($attrs['headermargin'])) {
		if ($attrs['headermargin'] === "0") {
			$wt_report->headermargin = 0;
		} elseif (!empty($attrs['headermargin'])) {
			$wt_report->headermargin = (int) $attrs['headermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
		}
	}
	// Footer Margin
	if (isset($attrs['footermargin'])) {
		if ($attrs['footermargin'] === "0") {
			$wt_report->footermargin = 0;
		} elseif (!empty($attrs['footermargin'])) {
			$wt_report->footermargin = (int) $attrs['footermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
		}
	}

	// Page Orientation
	if (!empty($attrs['orientation'])) {
		if ($attrs['orientation'] == "landscape") {
			$wt_report->orientation = "landscape";
		} elseif ($attrs['orientation'] == "portrait") {
			$wt_report->orientation = "portrait";
		}
	}
	// Page Size
	if (!empty($attrs['pageSize'])) {
		$wt_report->pageFormat = strtoupper($attrs['pageSize']);
	}

	// Show Generated By...
	if (isset($attrs['showGeneratedBy'])) {
		if ($attrs['showGeneratedBy'] === "0") {
			$wt_report->showGenText = false;
		} elseif ($attrs['showGeneratedBy'] === "1") {
			$wt_report->showGenText = true;
		}
	}

	$wt_report->setup();
}

/**
 * XML </Doc> end element handler
 *
 * @see DocSHandler()
 */
function DocEHandler() {
	global $wt_report;
	$wt_report->run();
}

/**
 * XML <Header> start element handler
 *
 * @see WT_Report_Base::setProcessing()
 */
function HeaderSHandler() {
	global $wt_report;

	// Clear the Header before any new elements are added
	$wt_report->clearHeader();
	$wt_report->setProcessing("H");
}

/**
 * XML <PageHeader> start element handler
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see PageHeaderEHandler()
 */
function PageHeaderSHandler($attrs) {
	global $printDataStack, $printData, $wt_reportStack, $wt_report, $ReportRoot;

	array_push($printDataStack, $printData);
	$printData = false;
	array_push($wt_reportStack, $wt_report);
	$wt_report = $ReportRoot->createPageHeader();
}

/**
 * XML <PageHeaderEHandler> end element handler
 *
 * @see PageHeaderSHandler()
 */
function PageHeaderEHandler() {
	global $printData, $printDataStack, $wt_report, $currentElement, $wt_reportStack;

	$printData      = array_pop($printDataStack);
	$currentElement = $wt_report;
	$wt_report      = array_pop($wt_reportStack);
	$wt_report->addElement($currentElement);
}

/**
 * XML <BodySHandler> start element handler
 */
function BodySHandler() {
	global $wt_report;
	$wt_report->setProcessing("B");
}

/**
 * XML <FooterSHandler> start element handler
 */
function FooterSHandler() {
	global $wt_report;
	$wt_report->setProcessing("F");
}

/**
 * XML <Cell> start element handler
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see  CellEHandler()
 * @see  Cell
 */
function CellSHandler($attrs) {
	global $printData, $printDataStack, $currentElement, $ReportRoot, $wt_report;

	// string The text alignment of the text in this box.
	$align = "";
	if (!empty($attrs['align'])) {
		$align = $attrs['align'];
		// RTL supported left/right alignment
		if ($align == "rightrtl") {
			if ($wt_report->rtl) {
				$align = "left";
			} else {
				$align = "right";
			}
		} elseif ($align == "leftrtl") {
			if ($wt_report->rtl) {
				$align = "right";
			} else {
				$align = "left";
			}
		}
	}

	// string The color to fill the background of this cell
	$bgcolor = "";
	if (!empty($attrs['bgcolor'])) {
		$bgcolor = $attrs['bgcolor'];
	}

	// int Whether or not the background should be painted
	$fill = 1;
	if (isset($attrs['fill'])) {
		if ($attrs['fill'] === "0") {
			$fill = 0;
		} elseif ($attrs['fill'] === "1") {
			$fill = 1;
		}
	}

	$reseth = true;
	// boolean   if true reset the last cell height (default true)
	if (isset($attrs['reseth'])) {
		if ($attrs['reseth'] === "0") {
			$reseth = false;
		} elseif ($attrs['reseth'] === "1") {
			$reseth = true;
		}
	}

	// mixed Whether or not a border should be printed around this box
	$border = 0;
	if (!empty($attrs['border'])) {
		$border = $attrs['border'];
	}
	// @test Print all borders for testing
	// $border = 1;
	// string Border color in HTML code
	$bocolor = "";
	if (!empty($attrs['bocolor'])) {
		$bocolor = $attrs['bocolor'];
	}

	// int Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted.
	$height = 0;
	if (!empty($attrs['height'])) {
		$height = (int) $attrs['height'];
	}
	// int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin.
	$width = 0;
	if (!empty($attrs['width'])) {
		$width = (int) $attrs['width'];
	}

	// int Stretch carachter mode
	$stretch = 0;
	if (!empty($attrs['stretch'])) {
		$stretch = (int) $attrs['stretch'];
	}

	// mixed Position the left corner of this box on the page. The default is the current position.
	$left = ".";
	if (isset($attrs['left'])) {
		if ($attrs['left'] === ".") {
			$left = ".";
		} elseif (!empty($attrs['left'])) {
			$left = (int) $attrs['left'];
		} elseif ($attrs['left'] === "0") {
			$left = 0;
		}
	}
	// mixed Position the top corner of this box on the page. the default is the current position
	$top = ".";
	if (isset($attrs['top'])) {
		if ($attrs['top'] === ".") {
			$top = ".";
		} elseif (!empty($attrs['top'])) {
			$top = (int) $attrs['top'];
		} elseif ($attrs['top'] === "0") {
			$top = 0;
		}
	}

	// string The name of the Style that should be used to render the text.
	$style = "";
	if (!empty($attrs['style'])) {
		$style = $attrs['style'];
	}

	// string Text color in html code
	$tcolor = "";
	if (!empty($attrs['tcolor'])) {
		$tcolor = $attrs['tcolor'];
	}

	// int Indicates where the current position should go after the call.
	$ln = 0;
	if (isset($attrs['newline'])) {
		if (!empty($attrs['newline'])) {
			$ln = (int) $attrs['newline'];
		} elseif ($attrs['newline'] === "0") {
			$ln = 0;
		}
	}

	if ($align == "left") {
		$align = "L";
	} elseif ($align == "right") {
		$align = "R";
	} elseif ($align == "center") {
		$align = "C";
	} elseif ($align == "justify") {
		$align = "J";
	}

	array_push($printDataStack, $printData);
	$printData = true;

	$currentElement = $ReportRoot->createCell(
		$width,
		$height,
		$border,
		$align,
		$bgcolor,
		$style,
		$ln,
		$top,
		$left,
		$fill,
		$stretch,
		$bocolor,
		$tcolor,
		$reseth
	);
}

/**
 * XML </Cell> end element handler
 *
 * @see CellSHandler()
 * @final
 */
function CellEHandler() {
	global $printData, $printDataStack, $currentElement, $wt_report;

	$printData = array_pop($printDataStack);
	$wt_report->addElement($currentElement);
}

/**
 * XML <Now /> element handler
 *
 * @see Element::addText()
 * @final
 */
function NowSHandler() {
	global $currentElement;

	$g = timestamp_to_gedcom_date(WT_CLIENT_TIMESTAMP);
	$currentElement->addText($g->Display());
}

/**
 * XML <PageNum /> element handler
 *
 * @see Element::addText()
 * @final
 */
function PageNumSHandler() {
	global $currentElement;
	$currentElement->addText("#PAGENUM#");
}

/**
 * XML <TotalPages /> element handler
 *
 * @see Element::addText()
 * @final
 */
function TotalPagesSHandler() {
	global $currentElement;
	$currentElement->addText("{{:ptp:}}");
}

/**
 * @see  GedcomEHandler()
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function GedcomSHandler($attrs) {
	global $vars, $gedrec, $gedrecStack, $processGedcoms, $fact, $desc, $ged_level;

	if ($processGedcoms > 0) {
		$processGedcoms++;

		return;
	}

	$id    = "";
	$match = array();
	if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
		$id = $match[1];
	}
	$tag       = $attrs['id'];
	$tag       = str_replace("@fact", $fact, $tag);
	$tags      = explode(":", $tag);
	$newgedrec = "";
	if (count($tags) < 2) {
		$tmp       = WT_GedcomRecord::getInstance($attrs['id']);
		$newgedrec = $tmp ? $tmp->privatizeGedcom(WT_USER_ACCESS_LEVEL) : '';
	}
	if (empty($newgedrec)) {
		$tgedrec   = $gedrec;
		$newgedrec = "";
		foreach ($tags as $tag) {
			if (preg_match("/\\$(.+)/", $tag, $match)) {
				if (isset($vars[$match[1]]['gedcom'])) {
					$newgedrec = $vars[$match[1]]['gedcom'];
				} else {
					$tmp       = WT_GedcomRecord::getInstance($match[1]);
					$newgedrec = $tmp ? $tmp->privatizeGedcom(WT_USER_ACCESS_LEVEL) : '';
				}
			} else {
				if (preg_match("/@(.+)/", $tag, $match)) {
					$gmatch = array();
					if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) {
						$tmp       = WT_GedcomRecord::getInstance($gmatch[1]);
						$newgedrec = $tmp ? $tmp->privatizeGedcom(WT_USER_ACCESS_LEVEL) : '';
						$tgedrec   = $newgedrec;
					} else {
						$newgedrec = "";
						break;
					}
				} else {
					$temp      = explode(" ", trim($tgedrec));
					$level     = $temp[0] + 1;
					$newgedrec = get_sub_record($level, "$level $tag", $tgedrec);
					$tgedrec   = $newgedrec;
				}
			}
		}
	}
	if (!empty($newgedrec)) {
		array_push($gedrecStack, array($gedrec, $fact, $desc));
		$gedrec = $newgedrec;
		if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $gedrec, $match)) {
			$ged_level = $match[1];
			$fact      = $match[2];
			$desc      = trim($match[3]);
		}
	} else {
		$processGedcoms++;
	}
}

/**
 * @see  GedcomSHandler()
 */
function GedcomEHandler() {
	global $gedrec, $gedrecStack, $processGedcoms, $fact, $desc;

	if ($processGedcoms > 0) {
		$processGedcoms--;
	} else {
		$temp   = array_pop($gedrecStack);
		$gedrec = $temp[0];
		$fact   = $temp[1];
		$desc   = $temp[2];
	}
}

/**
 * XML <TextBoxSHandler> start element handler
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see  TextBoxEHandler()
 */
function TextBoxSHandler($attrs) {
	global $printData, $printDataStack, $wt_report, $currentElement, $wt_reportStack, $ReportRoot;

	// string Background color code
	$bgcolor = "";
	if (!empty($attrs['bgcolor'])) {
		$bgcolor = $attrs['bgcolor'];
	}

	// boolean Wether or not fill the background color
	$fill = true;
	if (isset($attrs['fill'])) {
		if ($attrs['fill'] === "0") {
			$fill = false;
		} elseif ($attrs['fill'] === "1") {
			$fill = true;
		}
	}

	// var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
	$border = false;
	if (isset($attrs['border'])) {
		if ($attrs['border'] === "1") {
			$border = true;
		} elseif ($attrs['border'] === "0") {
			$border = false;
		}
	}
	// @test Print all borders for testing
	// $border = true;

	/**
	 * Border style of rectangle. Array with keys among the following
	 * <ul><li>L, T, R, B or combinations: Line style of left, top, right or bottom border.</li></ul>
	 *
	 * @var string
	 */
	/** not yet in use
	 * $borderstyle = "";
	 * if (!empty($attrs['borderstyle'])) $borderstyle = $attrs['borderstyle'];
	 */

	// int The starting height of this cell. If the text wraps the height will automatically be adjusted
	$height = 0;
	if (!empty($attrs['height'])) {
		$height = (int) $attrs['height'];
	}
	// int Setting the width to 0 will make it the width from the current location to the margin
	$width = 0;
	if (!empty($attrs['width'])) {
		$width = (int) $attrs['width'];
	}

	// mixed Position the left corner of this box on the page. The default is the current position.
	$left = ".";
	if (isset($attrs['left'])) {
		if ($attrs['left'] === ".") {
			$left = ".";
		} elseif (!empty($attrs['left'])) {
			$left = (int) $attrs['left'];
		} elseif ($attrs['left'] === "0") {
			$left = 0;
		}
	}
	// mixed Position the top corner of this box on the page. the default is the current position
	$top = ".";
	if (isset($attrs['top'])) {
		if ($attrs['top'] === ".") {
			$top = ".";
		} elseif (!empty($attrs['top'])) {
			$top = (int) $attrs['top'];
		} elseif ($attrs['top'] === "0") {
			$top = 0;
		}
	}
	// boolean After this box is finished rendering, should the next section of text start immediately after the this box or should it start on a new line under this box. 0 = no new line, 1 = force new line. Default is 0
	$newline = false;
	if (isset($attrs['newline'])) {
		if ($attrs['newline'] === "1") {
			$newline = true;
		} elseif ($attrs['newline'] === "0") {
			$newline = false;
		}
	}
	// boolean
	$pagecheck = true;
	if (isset($attrs['pagecheck'])) {
		if ($attrs['pagecheck'] === "0") {
			$pagecheck = false;
		} elseif ($attrs['pagecheck'] === "1") {
			$pagecheck = true;
		}
	}
	// boolean Cell padding
	$padding = true;
	if (isset($attrs['padding'])) {
		if ($attrs['padding'] === "0") {
			$padding = false;
		} elseif ($attrs['padding'] === "1") {
			$padding = true;
		}
	}
	// boolean Reset this box Height
	$reseth = false;
	if (isset($attrs['reseth'])) {
		if ($attrs['reseth'] === "1") {
			$reseth = true;
		} elseif ($attrs['reseth'] === "0") {
			$reseth = false;
		}
	}

	// string Style of rendering
	$style = "";
	// fill and border is enought for now for user input
	//if (!empty($attrs['style'])) $style = $attrs['style'];

	array_push($printDataStack, $printData);
	$printData = false;

	array_push($wt_reportStack, $wt_report);
	$wt_report = $ReportRoot->createTextBox(
		$width,
		$height,
		$border,
		$bgcolor,
		$newline,
		$left,
		$top,
		$pagecheck,
		$style,
		$fill,
		$padding,
		$reseth
	);
}

/**
 * XML <TextBoxEHandler> end element handler
 *
 * @see TextBoxSHandler()
 */
function TextBoxEHandler() {
	global $printData, $printDataStack, $wt_report, $currentElement, $wt_reportStack;

	$printData      = array_pop($printDataStack);
	$currentElement = $wt_report;
	$wt_report      = array_pop($wt_reportStack);
	$wt_report->addElement($currentElement);
}

/**
 * @see  TextEHandler()
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function TextSHandler($attrs) {
	global $printData, $printDataStack, $currentElement, $ReportRoot;

	array_push($printDataStack, $printData);
	$printData = true;

	// string The name of the Style that should be used to render the text.
	$style = "";
	if (!empty($attrs['style'])) {
		$style = $attrs['style'];
	}

	// string  The color of the text - Keep the black color as default
	$color = "";
	if (!empty($attrs['color'])) {
		$color = $attrs['color'];
	}

	$currentElement = $ReportRoot->createText($style, $color);
}

/**
 * @see TextSHandler()
 */
function TextEHandler() {
	global $printData, $printDataStack, $wt_report, $currentElement;

	$printData = array_pop($printDataStack);
	$wt_report->addElement($currentElement);
}

/**
 * XML <GetPersonName> start element handler
 * Get the name
 * 1. id is empty - current GEDCOM record
 * 2. id is set with a record id
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function GetPersonNameSHandler($attrs) {
	// @deprecated
	global $currentElement, $vars, $gedrec;

	$id    = "";
	$match = array();
	if (empty($attrs['id'])) {
		if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
			$id = $match[1];
		}
	} else {
		if (preg_match("/\\$(.+)/", $attrs['id'], $match)) {
			if (isset($vars[$match[1]]['id'])) {
				$id = $vars[$match[1]]['id'];
			}
		} else {
			if (preg_match("/@(.+)/", $attrs['id'], $match)) {
				$gmatch = array();
				if (preg_match("/\d $match[1] @([^@]+)@/", $gedrec, $gmatch)) {
					$id = $gmatch[1];
				}
			} else {
				$id = $attrs['id'];
			}
		}
	}
	if (!empty($id)) {
		$record = WT_GedcomRecord::getInstance($id);
		if (is_null($record)) {
			return;
		}
		if (!$record->canShowName()) {
			$currentElement->addText(WT_I18N::translate('Private'));
		} else {
			$name = $record->getFullName();
			$name = preg_replace(
				array('/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'),
				array('«', '', '»'),
				$name
			);
			if (!WT_RNEW) {
				$name = strip_tags($name);
			}
			if (!empty($attrs['truncate'])) {
				//short-circuit with the faster strlen
				if (strlen($name) > $attrs['truncate'] && WT_I18N::strlen($name) > $attrs['truncate']) {
					$name  = preg_replace("/\(.*\) ?/", "", $name); //removes () and text inbetween - what about ", [ and { etc?
					$words = preg_split('/[, -]+/', $name); // names separated with space, comma or hyphen - any others?
					$name  = $words[count($words) - 1];
					for ($i = count($words) - 2; $i >= 0; $i--) {
						$len = WT_I18N::strlen($name);
						for ($j = count($words) - 3; $j >= 0; $j--) {
							$len += WT_I18N::strlen($words[$j]);
						}
						if ($len > $attrs['truncate']) {
							$first_letter = WT_I18N::substr($words[$i], 0, 1);
							//do not show " of nick-names
							if ($first_letter != "\"") {
								$name = WT_I18N::substr($words[$i], 0, 1) . ". " . $name;
							}
						} else {
							$name = $words[$i] . " " . $name;
						}
					}
				}
			} else {
				$addname = $record->getAddName();
				/*
				echo "<br>".$addname."<br>";
				for ($ii=0; $ii<=strlen($addname); $ii++)
				echo substr($addname, $ii, 1)." ";
				*/
				$addname = preg_replace(
					array('/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'),
					array('«', '', '»'),
					$addname
				);
				if (!WT_RNEW) {
					$addname = strip_tags($addname); //@@ unknown printed in other alignment with ... on wrong side
				}
				if (!empty($addname)) {
					$name .= " " . $addname;
				}
			}
			$currentElement->addText(trim($name));
		}
	}
}

/**
 * XML <GedcomValue> start element handler
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function GedcomValueSHandler($attrs) {
	// @deprecated
	global $currentElement, $gedrec, $fact, $desc;

	$id    = "";
	$match = array();
	if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
		$id = $match[1];
	}

	if (isset($attrs['newline']) && $attrs['newline'] == "1") {
		$useBreak = "1";
	} else {
		$useBreak = "0";
	}

	$tag = $attrs['tag'];
	if (!empty($tag)) {
		if ($tag == "@desc") {
			$value = $desc;
			$value = trim($value);
			$currentElement->addText($value);
		}
		if ($tag == "@id") {
			$currentElement->addText($id);
		} else {
			$tag = str_replace("@fact", $fact, $tag);
			if (empty($attrs['level'])) {
				$temp  = explode(" ", trim($gedrec));
				$level = $temp[0];
				if ($level == 0) {
					$level++;
				}
			} else {
				$level = $attrs['level'];
			}
			$truncate = "";
			if (isset($attrs['truncate'])) {
				$truncate = $attrs['truncate'];
			}
			$tags  = preg_split('/[: ]/', $tag);
			$value = get_gedcom_value($tag, $level, $gedrec, $truncate);
			switch (end($tags)) {
			case 'DATE':
				$tmp   = new WT_Date($value);
				$value = $tmp->Display();
				break;
			case 'PLAC':
				$tmp   = new WT_Place($value, WT_GED_ID);
				$value = $tmp->getShortName();
				break;
			}
			if ($useBreak == "1") {
				// Insert <br> when multiple dates exist.
				// This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
				$value = str_replace('(', '<br>(', $value);
				$value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
				$value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
				if (substr($value, 0, 6) == '<br>') {
					$value = substr($value, 6);
				}
			}
			$currentElement->addText($value);
		}
	}
}

/**
 * XML <RepeatTag> start element handler
 *
 * @see RepeatTagEHandler()
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function RepeatTagSHandler($attrs) {
	// @deprecated
	global $repeats, $repeatsStack, $gedrec, $repeatBytes, $parser, $processRepeats, $fact, $desc;

	$processRepeats++;
	if ($processRepeats > 1) {
		return;
	}

	array_push($repeatsStack, array($repeats, $repeatBytes));
	$repeats     = array();
	$repeatBytes = xml_get_current_line_number($parser);

	$id    = "";
	$match = array();
	if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
		$id = $match[1];
	}

	$tag = "";
	if (isset($attrs['tag'])) {
		$tag = $attrs['tag'];
	}
	if (!empty($tag)) {
		if ($tag == "@desc") {
			$value = $desc;
			$value = trim($value);
			$currentElement->addText($value);
		} else {
			$tag   = str_replace("@fact", $fact, $tag);
			$tags  = explode(":", $tag);
			$temp  = explode(" ", trim($gedrec));
			$level = $temp[0];
			if ($level == 0) {
				$level++;
			}
			$subrec = $gedrec;
			$t      = $tag;
			$count  = count($tags);
			$i      = 0;
			while ($i < $count) {
				$t = $tags[$i];
				if (!empty($t)) {
					if ($i < ($count - 1)) {
						$subrec = get_sub_record($level, "$level $t", $subrec);
						if (empty($subrec)) {
							$level--;
							$subrec = get_sub_record($level, "@ $t", $gedrec);
							if (empty($subrec)) {
								return;
							}
						}
					}
					$level++;
				}
				$i++;
			}
			$level--;
			$count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
			$i     = 0;
			while ($i < $count) {
				$repeats[] = get_sub_record($level, "$level $t", $subrec, $i + 1);;
				$i++;
			}
		}
	}
}

/**
 * XML </ RepeatTag> end element handler
 *
 * @see RepeatTagSHandler()
 */
function RepeatTagEHandler() {
	global $processRepeats, $repeats, $repeatsStack, $repeatBytes;

	$processRepeats--;
	if ($processRepeats > 0) {
		return;
	}

	// Check if there is anything to repeat
	if (count($repeats) > 0) {
		// No need to load them if not used...
		global $parser, $parserStack, $report, $gedrec;
		// @deprecated
		//$line = xml_get_current_line_number($parser)-1;
		$lineoffset = 0;
		foreach ($repeatsStack as $rep) {
			$lineoffset += $rep[1];
		}
		//-- read the xml from the file
		$lines = file($report);
		while (strpos($lines[$lineoffset + $repeatBytes], "<RepeatTag") === false) {
			$lineoffset--;
		}
		$lineoffset++;
		$reportxml = "<tempdoc>\n";
		$line_nr   = $lineoffset + $repeatBytes;
		// RepeatTag Level counter
		$count = 1;
		while (0 < $count) {
			if (strstr($lines[$line_nr], "<RepeatTag") !== false) {
				$count++;
			} elseif (strstr($lines[$line_nr], "</RepeatTag") !== false) {
				$count--;
			}
			if (0 < $count) {
				$reportxml .= $lines[$line_nr];
			}
			$line_nr++;
		}
		// No need to drag this
		unset($lines);
		$reportxml .= "</tempdoc>\n";
		// Save original values
		array_push($parserStack, $parser);
		$oldgedrec = $gedrec;
		foreach ($repeats as $gedrec) {
			//-- start the sax parser
			$repeat_parser = xml_parser_create();
			$parser        = $repeat_parser;
			//-- make sure everything is case sensitive
			xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
			//-- set the main element handler functions
			xml_set_element_handler($repeat_parser, "startElement", "endElement");
			//-- set the character data handler
			xml_set_character_data_handler($repeat_parser, "characterData");
			if (!xml_parse($repeat_parser, $reportxml, true)) {
				printf(
					$reportxml . "\nRepeatTagEHandler XML error: %s at line %d",
					xml_error_string(xml_get_error_code($repeat_parser)),
					xml_get_current_line_number($repeat_parser)
				);
				exit;
			}
			xml_parser_free($repeat_parser);
		}
		// Restore original values
		$gedrec = $oldgedrec;
		$parser = array_pop($parserStack);
	}
	$temp        = array_pop($repeatsStack);
	$repeats     = $temp[0];
	$repeatBytes = $temp[1];
}

/**
 * Variable lookup
 *
 * Retrieve predefined variables :
 * @ desc GEDCOM fact description, example:
 *        1 EVEN This is a description
 * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
 * $ WT_I18N::translate('....')
 * $ language_settings[]
 *
 *
 * Or retrieve variables preset with <SetVar> element
 *
 * If the variable is a date and 'date="1"' attribute is set then the date will be reformated
 * from Sep to September
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see SetVarSHandler()
 */
function varSHandler($attrs) {
	// @deprecated
	global $currentElement, $type, $parser;
	// Retrievable variables
	global $desc, $fact, $language_settings, $vars;

	if (empty($attrs['var'])) {
		die("<strong>REPORT ERROR var: </strong> The attribute \"var=\" is missing or not set in the XML file on line: " . xml_get_current_line_number(
				$parser
			));
	}

	$var = $attrs['var'];
	// SetVar element preset variables
	if (!empty($vars[$var]['id'])) {
		$var = $vars[$var]['id'];
	} else {
		$tfact = $fact;
		if (($fact == "EVEN" || $fact == "FACT") && is_string($type) && $type != " ") {
			// Use :
			// n TYPE This text if string
			$tfact = $type;
		}
		$var = str_replace(array("@fact", "@desc"), array(WT_Gedcom_Tag::getLabel($tfact), $desc), $var);
		if (substr($var, 0, 18) == 'WT_I18N::translate' || substr($var, 0, 15) == 'WT_I18N::number') {
			eval("\$var=$var;");
		}
	}
	// Check if variable is set as a date and reformat the date
	if (isset($attrs['date'])) {
		if ($attrs['date'] === "1") {
			$g   = new WT_Date($var);
			$var = $g->Display();
		}
	}
	$currentElement->addText($var);
}

/**
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see  FactsEHandler()
 */
function FactsSHandler($attrs) {
	// @deprecated
	global $repeats, $repeatsStack, $gedrec, $parser, $repeatBytes, $processRepeats, $vars;

	$processRepeats++;
	if ($processRepeats > 1) {
		return;
	}

	array_push($repeatsStack, array($repeats, $repeatBytes));
	$repeats     = array();
	$repeatBytes = xml_get_current_line_number($parser);

	$id    = "";
	$match = array();
	if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
		$id = $match[1];
	}
	$tag = "";
	if (isset($attrs['ignore'])) {
		$tag .= $attrs['ignore'];
	}
	if (preg_match("/\\$(.+)/", $tag, $match)) {
		$tag = $vars[$match[1]]['id'];
	}

	$record = WT_GedcomRecord::getInstance($id);
	if (empty($attrs['diff']) && !empty($id)) {
		$facts = $record->getFacts();
		sort_facts($facts);
		$repeats  = array();
		$nonfacts = explode(',', $tag);
		foreach ($facts as $event) {
			if (!in_array($event->getTag(), $nonfacts)) {
				$repeats[] = $event->getGedcom();
			}
		}
	} else {
		foreach ($record->getFacts() as $fact) {
			if ($fact->isNew() && $fact->getTag() <> 'CHAN') {
				$repeats[] = $fact->getGedcom();
			}
		}
	}
}

/**
 * XML </ Facts> end element handler
 *
 * @see FactsSHandler()
 */
function FactsEHandler() {
	global $repeats, $repeatsStack, $repeatBytes, $parser, $parserStack, $report, $gedrec, $fact, $desc, $type, $processRepeats;

	$processRepeats--;
	if ($processRepeats > 0) {
		return;
	}

	// Check if there is anything to repeat
	if (count($repeats) > 0) {

		$line       = xml_get_current_line_number($parser) - 1;
		$lineoffset = 0;
		foreach ($repeatsStack as $rep) {
			$lineoffset += $rep[1];
		}

		//-- read the xml from the file
		$lines = file($report);
		while ($lineoffset + $repeatBytes > 0 && strpos($lines[$lineoffset + $repeatBytes], '<Facts ') === false) {
			$lineoffset--;
		}
		$lineoffset++;
		$reportxml = "<tempdoc>\n";
		$i         = $line + $lineoffset;
		$line_nr   = $repeatBytes + $lineoffset;
		while ($line_nr < $i) {
			$reportxml .= $lines[$line_nr];
			$line_nr++;
		}
		// No need to drag this
		unset($lines);
		$reportxml .= "</tempdoc>\n";
		// Save original values
		array_push($parserStack, $parser);
		$oldgedrec = $gedrec;
		$count     = count($repeats);
		$i         = 0;
		$match     = array();
		while ($i < $count) {
			$gedrec = $repeats[$i];
			$fact   = "";
			$desc   = "";
			if (preg_match("/1 (\w+)(.*)/", $gedrec, $match)) {
				$fact = $match[1];
				if ($fact == "EVEN" or $fact == "FACT") {
					$tmatch = array();
					if (preg_match("/2 TYPE (.+)/", $gedrec, $tmatch)) {
						$type = trim($tmatch[1]);
					} else {
						$type = " ";
					}
				}
				$desc = trim($match[2]);
				$desc .= get_cont(2, $gedrec);
			}
			//-- start the sax parser
			$repeat_parser = xml_parser_create();
			$parser        = $repeat_parser;
			//-- make sure everything is case sensitive
			xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
			//-- set the main element handler functions
			xml_set_element_handler($repeat_parser, "startElement", "endElement");
			//-- set the character data handler
			xml_set_character_data_handler($repeat_parser, "characterData");
			if (!xml_parse($repeat_parser, $reportxml, true)) {
				die(sprintf(
					$reportxml . "\nFactsEHandler XML error: %s at line %d",
					xml_error_string(xml_get_error_code($repeat_parser)),
					xml_get_current_line_number($repeat_parser)
				));
			}
			xml_parser_free($repeat_parser);
			$i++;
		}
		// Restore original values
		$parser = array_pop($parserStack);
		$gedrec = $oldgedrec;
	}
	$temp        = array_pop($repeatsStack);
	$repeats     = $temp[0];
	$repeatBytes = $temp[1];
}

/**
 * Setting upp or changing variables in the XML
 * The XML variable name and value is stored in the global variable $vars
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function SetVarSHandler($attrs) {
	global $vars, $gedrec, $fact, $desc, $type, $generation;

	if (empty($attrs['name'])) {
		die("<strong>REPORT ERROR var: </strong> The attribute \"name=\" is missing or not set in the XML file");
	}

	$name  = $attrs['name'];
	$value = $attrs['value'];
	$match = array();
	// Current GEDCOM record strings
	if ($value == "@ID") {
		if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
			$value = $match[1];
		}
	} elseif ($value == "@fact") {
		$value = $fact;
	} elseif ($value == "@desc") {
		$value = $desc;
	} elseif ($value == "@generation") {
		$value = $generation;
	} elseif (preg_match("/@(\w+)/", $value, $match)) {
		$gmatch = array();
		if (preg_match("/\d $match[1] (.+)/", $gedrec, $gmatch)) {
			$value = str_replace("@", "", trim($gmatch[1]));
		}
	}
	if (preg_match("/\\$(\w+)/", $name, $match)) {
		$name = $vars["'" . $match[1] . "'"]['id'];
	}
	$count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
	$i     = 0;
	while ($i < $count) {
		$t     = $vars[$match[$i][1]]['id'];
		$value = preg_replace("/\\$" . $match[$i][1] . "/", $t, $value, 1);
		$i++;
	}
	if (substr($value, 0, 18) == 'WT_I18N::translate' || substr($value, 0, 15) == 'WT_I18N::number') {
		eval("\$value = $value;");
	}
	// Arithmetic functions
	if (preg_match("/(\d+)\s*([\-\+\*\/])\s*(\d+)/", $value, $match)) {
		switch ($match[2]) {
		case "+":
			$t     = $match[1] + $match[3];
			$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
			break;
		case "-":
			$t     = $match[1] - $match[3];
			$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
			break;
		case "*":
			$t     = $match[1] * $match[3];
			$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
			break;
		case "/":
			$t     = $match[1] / $match[3];
			$value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value);
			break;
		}
	}
	if (strpos($value, "@") !== false) {
		$value = "";
	}
	$vars[$name]['id'] = $value;
}

/**
 * XML <if > start element
 *
 * @see ifEHandler()
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function ifSHandler($attrs) {
	global $vars, $gedrec, $processIfs, $fact, $desc, $generation;

	if ($processIfs > 0) {
		$processIfs++;

		return;
	}

	$condition = $attrs['condition'];
	$condition = preg_replace("/\\$(\w+)/", "\$vars[\"$1\"][\"id\"]", $condition);
	$condition = str_replace(array(" LT ", " GT "), array("<", ">"), $condition);
	// Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
	$condition = str_replace("@fact", $fact, $condition);
	$match     = array();
	$count     = preg_match_all("/@([\w:\.]+)/", $condition, $match, PREG_SET_ORDER);
	$i         = 0;
	while ($i < $count) {
		$id    = $match[$i][1];
		$value = "\"\"";
		if ($id == "ID") {
			if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
				$value = "'" . $match[1] . "'";
			}
		} elseif ($id == "fact") {
			$value = "\"$fact\"";
		} elseif ($id == "desc") {
			$value = "\"" . addslashes($desc) . "\"";
		} elseif ($id == "generation") {
			$value = "\"$generation\"";
		} else {

			$temp  = explode(" ", trim($gedrec));
			$level = $temp[0];
			if ($level == 0) {
				$level++;
			}
			$value = get_gedcom_value($id, $level, $gedrec);
			if (empty($value)) {
				$level++;
				$value = get_gedcom_value($id, $level, $gedrec);
			}
			$value = "\"" . addslashes($value) . "\"";
		}
		$condition = str_replace("@$id", $value, $condition);
		$i++;
	}
	$condition = "if ($condition) return true; else return false;";
	$ret       = @eval($condition);
	if (!$ret) {
		$processIfs++;
	}
}

/**
 * XML <if /> end element
 *
 * @see ifSHandler()
 */
function ifEHandler() {
	global $processIfs;
	if ($processIfs > 0) {
		$processIfs--;
	}
}

/**
 * XML <Footnote > start element
 * Collect the Footnote links
 * GEDCOM Records that are protected by Privacy setting will be ignore
 *
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see FootnoteEHandler()
 */
function FootnoteSHandler($attrs) {
	global $printData, $printDataStack, $currentElement, $footnoteElement, $processFootnote, $gedrec, $ReportRoot;

	$match = array();
	$id    = "";
	$tag   = "";
	if (preg_match("/[0-9] (.+) @(.+)@/", $gedrec, $match)) {
		$tag = $match[1];
		$id  = $match[2];
	}
	$record = WT_GedcomRecord::GetInstance($id);
	if ($record && $record->canShow()) {
		array_push($printDataStack, $printData);
		$printData = true;
		$style     = "";
		if (!empty($attrs['style'])) {
			$style = $attrs['style'];
		}
		$footnoteElement = $currentElement;
		$currentElement  = $ReportRoot->createFootnote($style);
	} else {
		$printData       = false;
		$processFootnote = false;
	}
}

/**
 * XML <Footnote /> end element
 * Print the collected Footnote data
 *
 * @see FootnoteSHandler()
 */
function FootnoteEHandler() {
	// @deprecated
	global $printData, $printDataStack, $currentElement, $footnoteElement, $processFootnote, $wt_report;

	if ($processFootnote) {
		$printData = array_pop($printDataStack);
		$temp      = trim($currentElement->getValue());
		if (strlen($temp) > 3) {
			$wt_report->addElement($currentElement);
		}
		$currentElement = $footnoteElement;
	} else {
		$processFootnote = true;
	}
}

/**
 * XML <FootnoteTexts /> element
 */
function FootnoteTextsSHandler() {
	global $wt_report;

	$temp = "footnotetexts";
	$wt_report->addElement($temp);
}

/**
 * XML <AgeAtDeath /> element handler
 *
 * @see Element::addText()
 * @final
 */
function AgeAtDeathSHandler() {
	// TODO: This duplicates functionality in format_fact_date()
	global $currentElement, $gedrec, $fact, $desc;

	$id    = "";
	$match = array();
	if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
		$person = WT_Individual::getInstance($match[1]);
		// Recorded age
		if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) {
			$fact_age = $match[1];
		} else {
			$fact_age = '';
		}
		if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) {
			$husb_age = $match[1];
		} else {
			$husb_age = '';
		}
		if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) {
			$wife_age = $match[1];
		} else {
			$wife_age = '';
		}

		// Calculated age
		$birth_date = $person->getBirthDate();
		// Can't use getDeathDate(), as this also gives BURI/CREM events, which
		// wouldn't give the correct "days after death" result for people with
		// no DEAT.
		$death_event = $person->getFirstFact('DEAT');
		if ($death_event) {
			$death_date = $death_event->getDate();
		} else {
			$death_date = new WT_Date('');
		}
		$value = '';
		if (WT_Date::Compare($birth_date, $death_date) <= 0 || !$person->isDead()) {
			$age = WT_Date::GetAgeGedcom($birth_date, $death_date);
			// Only show calculated age if it differs from recorded age
			if ($age != '' && $age != "0d") {
				if ($fact_age != '' && $fact_age != $age || $fact_age == '' && $husb_age == '' && $wife_age == '' || $husb_age != '' && $person->getSex(
					) == 'M' && $husb_age != $age || $wife_age != '' && $person->getSex() == 'F' && $wife_age != $age
				) {
					$value  = get_age_at_event($age, false);
					$abbrev = substr($value, 0, strpos($value, ' ') + 5);
					if ($value !== $abbrev) {
						$value = $abbrev . '.';
					}
				}
			}
		}
		$currentElement->addText($value);
	}
}

/**
 * XML element Forced line break handler - HTML code
 *
 */
function brSHandler() {
	global $printData, $currentElement, $processGedcoms;
	if ($printData && ($processGedcoms == 0)) {
		$currentElement->addText('<br>');
	}
}

/**
 * XML <sp />element Forced space handler
 */
function spSHandler() {
	global $printData, $currentElement, $processGedcoms;
	if ($printData && ($processGedcoms == 0)) {
		$currentElement->addText(' ');
	}
}

/**
 * @param array $attrs an array of key value pairs for the attributes
 */
function HighlightedImageSHandler($attrs) {
	global $gedrec, $wt_report, $ReportRoot;

	$id    = '';
	$match = array();
	if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
		$id = $match[1];
	}

	// mixed Position the top corner of this box on the page. the default is the current position
	$top = '.';
	if (isset($attrs['top'])) {
		if ($attrs['top'] === '0') {
			$top = 0;
		} elseif ($attrs['top'] === '.') {
			$top = '.';
		} elseif (!empty($attrs['top'])) {
			$top = (int) $attrs['top'];
		}
	}

	// mixed Position the left corner of this box on the page. the default is the current position
	$left = '.';
	if (isset($attrs['left'])) {
		if ($attrs['left'] === '0') {
			$left = 0;
		} elseif ($attrs['left'] === '.') {
			$left = '.';
		} elseif (!empty($attrs['left'])) {
			$left = (int) $attrs['left'];
		}
	}

	// string Align the image in left, center, right
	$align = '';
	if (!empty($attrs['align'])) {
		$align = $attrs['align'];
	}

	// string Next Line should be T:next to the image, N:next line
	$ln = '';
	if (!empty($attrs['ln'])) {
		$ln = $attrs['ln'];
	}

	$width  = 0;
	$height = 0;
	if (!empty($attrs['width'])) {
		$width = (int) $attrs['width'];
	}
	if (!empty($attrs['height'])) {
		$height = (int) $attrs['height'];
	}

	$person      = WT_Individual::getInstance($id);
	$mediaobject = $person->findHighlightedMedia();
	if ($mediaobject) {
		$attributes = $mediaobject->getImageAttributes('thumb');
		if (in_array(
				$attributes['ext'],
				array(
					'GIF',
					'JPG',
					'PNG',
					'SWF',
					'PSD',
					'BMP',
					'TIFF',
					'TIFF',
					'JPC',
					'JP2',
					'JPX',
					'JB2',
					'SWC',
					'IFF',
					'WBMP',
					'XBM'
				)
			) && $mediaobject->canShow() && $mediaobject->fileExists('thumb')
		) {
			if (($width > 0) and ($height == 0)) {
				$perc   = $width / $attributes['adjW'];
				$height = round($attributes['adjH'] * $perc);
			} elseif ($height > 0 && $width == 0) {
				$perc  = $height / $attributes['adjH'];
				$width = round($attributes['adjW'] * $perc);
			} else {
				$width  = $attributes['adjW'];
				$height = $attributes['adjH'];
			}
			$image = $ReportRoot->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
			$wt_report->addElement($image);
		}
	}
}

/**
 * @param array $attrs an array of key value pairs for the attributes
 */
function ImageSHandler($attrs) {
	global $gedrec, $wt_report, $MEDIA_DIRECTORY, $ReportRoot;

	// mixed Position the top corner of this box on the page. the default is the current position
	$top = '.';
	if (isset($attrs['top'])) {
		if ($attrs['top'] === "0") {
			$top = 0;
		} elseif ($attrs['top'] === '.') {
			$top = '.';
		} elseif (!empty($attrs['top'])) {
			$top = (int) $attrs['top'];
		}
	}

	// mixed Position the left corner of this box on the page. the default is the current position
	$left = '.';
	if (isset($attrs['left'])) {
		if ($attrs['left'] === '0') {
			$left = 0;
		} elseif ($attrs['left'] === '.') {
			$left = '.';
		} elseif (!empty($attrs['left'])) {
			$left = (int) $attrs['left'];
		}
	}

	// string Align the image in left, center, right
	$align = '';
	if (!empty($attrs['align'])) {
		$align = $attrs['align'];
	}

	// string Next Line should be T:next to the image, N:next line
	$ln = 'T';
	if (!empty($attrs['ln'])) {
		$ln = $attrs['ln'];
	}

	$width  = 0;
	$height = 0;
	if (!empty($attrs['width'])) {
		$width = (int) $attrs['width'];
	}
	if (!empty($attrs['height'])) {
		$height = (int) $attrs['height'];
	}

	$file = '';
	if (!empty($attrs['file'])) {
		$file = $attrs['file'];
	}
	if ($file == "@FILE") {
		$match = array();
		if (preg_match("/\d OBJE @(.+)@/", $gedrec, $match)) {
			$mediaobject = WT_Media::getInstance($match[1], WT_GED_ID);
			$attributes  = $mediaobject->getImageAttributes('thumb');
			if (in_array(
					$attributes['ext'],
					array(
						'GIF',
						'JPG',
						'PNG',
						'SWF',
						'PSD',
						'BMP',
						'TIFF',
						'TIFF',
						'JPC',
						'JP2',
						'JPX',
						'JB2',
						'SWC',
						'IFF',
						'WBMP',
						'XBM'
					)
				) && $mediaobject->canShow() && $mediaobject->fileExists('thumb')
			) {
				if (($width > 0) and ($height == 0)) {
					$perc   = $width / $attributes['adjW'];
					$height = round($attributes['adjH'] * $perc);
				} elseif ($height > 0 && $width == 0) {
					$perc  = $height / $attributes['adjH'];
					$width = round($attributes['adjW'] * $perc);
				} else {
					$width  = $attributes['adjW'];
					$height = $attributes['adjH'];
				}
				$image = $ReportRoot->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln);
				$wt_report->addElement($image);
			}
		}
	} else {
		if (file_exists($file) && preg_match("/(jpg|jpeg|png|gif)$/i", $file)) {
			$size = getimagesize($file);
			if (($width > 0) and ($height == 0)) {
				$perc   = $width / $size[0];
				$height = round($size[1] * $perc);
			} elseif ($height > 0 && $width == 0) {
				$perc  = $height / $size[1];
				$width = round($size[0] * $perc);
			} else {
				$width  = $size[0];
				$height = $size[1];
			}
			$image = $ReportRoot->createImage($file, $left, $top, $width, $height, $align, $ln);
			$wt_report->addElement($image);
		}
	}
}

/**
 * XML <Line> element handler
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function LineSHandler($attrs) {
	global $wt_report, $ReportRoot;

	// Start horizontal position, current position (default)
	$x1 = ".";
	if (isset($attrs['x1'])) {
		if ($attrs['x1'] === "0") {
			$x1 = 0;
		} elseif ($attrs['x1'] === ".") {
			$x1 = ".";
		} elseif (!empty($attrs['x1'])) {
			$x1 = (int) $attrs['x1'];
		}
	}
	// Start vertical position, current position (default)
	$y1 = ".";
	if (isset($attrs['y1'])) {
		if ($attrs['y1'] === "0") {
			$y1 = 0;
		} elseif ($attrs['y1'] === ".") {
			$y1 = ".";
		} elseif (!empty($attrs['y1'])) {
			$y1 = (int) $attrs['y1'];
		}
	}
	// End horizontal position, maximum width (default)
	$x2 = ".";
	if (isset($attrs['x2'])) {
		if ($attrs['x2'] === "0") {
			$x2 = 0;
		} elseif ($attrs['x2'] === ".") {
			$x2 = ".";
		} elseif (!empty($attrs['x2'])) {
			$x2 = (int) $attrs['x2'];
		}
	}
	// End vertical position
	$y2 = ".";
	if (isset($attrs['y2'])) {
		if ($attrs['y2'] === "0") {
			$y2 = 0;
		} elseif ($attrs['y2'] === ".") {
			$y2 = ".";
		} elseif (!empty($attrs['y2'])) {
			$y2 = (int) $attrs['y2'];
		}
	}

	$line = $ReportRoot->createLine($x1, $y1, $x2, $y2);
	$wt_report->addElement($line);
}

/**
 * XML <List> start element handler
 *
 * @see ListEHandler()
 *
 * @param array $attrs an array of key value pairs for the attributes
 */
function ListSHandler($attrs) {
	global $gedrec, $repeats, $repeatBytes, $list, $repeatsStack, $processRepeats, $parser, $vars, $sortby;
	global $GEDCOM;

	$processRepeats++;
	if ($processRepeats > 1) {
		return;
	}

	$match = array();
	if (isset($attrs['sortby'])) {
		$sortby = $attrs['sortby'];
		if (preg_match("/\\$(\w+)/", $sortby, $match)) {
			$sortby = $vars[$match[1]]['id'];
			$sortby = trim($sortby);
		}
	} else {
		$sortby = "NAME";
	}

	if (isset($attrs['list'])) {
		$listname = $attrs['list'];
	} else {
		$listname = "individual";
	}
	// Some filters/sorts can be applied using SQL, while others require PHP
	switch ($listname) {
	case "pending":
		$rows = WT_DB::prepare(
			"SELECT xref, gedcom_id, CASE new_gedcom WHEN '' THEN old_gedcom ELSE new_gedcom END AS gedcom" . " FROM `##change`" . " WHERE (xref, change_id) IN (" . "  SELECT xref, MAX(change_id)" . "   FROM `##change`" . "   WHERE status='pending' AND gedcom_id=?" . "   GROUP BY xref" . " )"
		)->execute(array(WT_GED_ID))->fetchAll();
		$list = array();
		foreach ($rows as $row) {
			$list[] = WT_GedcomRecord::getInstance($row->xref, $row->gedcom_id, $row->gedcom);
		}
		break;
	case "individual":
	case "family":
		$sql_col_prefix = substr($listname, 0, 1) . "_"; // i_ for individual, f_ for family, etc.
		$sql_join       = array();
		$sql_where      = array($sql_col_prefix . "file=" . WT_GED_ID);
		$sql_order_by   = array();
		foreach ($attrs as $attr => $value) {
			if ((strpos($attr, "filter") === 0) && $value) {
				// Substitute global vars
				$value = preg_replace_callback(
					'/\$(\w+)/',
					function ($matches) use ($vars) {
						return $vars[$matches[1]]['id'];
					},
					$value
				);
				// Convert the various filters into SQL
				if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
					$sql_join[]  = "JOIN `##dates` AS {$attr} ON ({$attr}.d_file={$sql_col_prefix}file AND {$attr}.d_gid={$sql_col_prefix}id)";
					$sql_where[] = "{$attr}.d_fact='{$match[1]}'";
					$date        = new WT_Date($match[3]);
					if ($match[2] == "LTE") {
						$sql_where[] = "{$attr}.d_julianday2<=" . $date->minJD();
					} else {
						$sql_where[] = "{$attr}.d_julianday1>=" . $date->minJD();
					}
					if ($sortby == $match[1]) {
						$sortby         = "";
						$sql_order_by[] = "{$attr}.d_julianday1";
					}
					unset($attrs[$attr]); // This filter has been fully processed
				} elseif (($listname == "individual") && (preg_match('/^NAME CONTAINS (.*)$/', $value, $match))) {
					// Do nothing, unless you have to
					if (($match[1] != "") or ($sortby == "NAME")) {
						$sql_join[] = "JOIN `##name` AS {$attr} ON (n_file={$sql_col_prefix}file AND n_id={$sql_col_prefix}id)";
						// Search the DB only if there is any name supplied
						if ($match[1] != "") {
							$names = explode(" ", $match[1]);
							foreach ($names as $name) {
								$sql_where[] = "{$attr}.n_full LIKE " . WT_DB::quote("%{$name}%");
							}
						}
						// Let the DB do the name sorting even when no name was entered
						if ($sortby == "NAME") {
							$sortby         = "";
							$sql_order_by[] = "{$attr}.n_sort";
						}
					}
					unset($attrs[$attr]); // This filter has been fully processed
				} elseif (($listname == "individual") && (preg_match('/^REGEXP \/(.+)\//', $value, $match))) {
					$sql_where[] = "i_gedcom REGEXP '" . $match[1] . "'";
					unset($attrs[$attr]); // This filter has been fully processed
				} elseif (($listname == "family") && (preg_match('/^REGEXP \/(.+)\//', $value, $match))) {
					$sql_where[] = "f_gedcom REGEXP '" . $match[1] . "'";
					unset($attrs[$attr]); // This filter has been fully processed
				} elseif (($listname == "family") && (preg_match('/^NAME CONTAINS (.+)$/', $value, $match))) {
					// Eventually, family "names" will be stored in wt_name.  Until then, an extra is needed....
					$sql_join[]  = "JOIN `##link` AS {$attr}a ON ({$attr}a.l_file={$sql_col_prefix}file AND {$attr}a.l_from={$sql_col_prefix}id)";
					$sql_join[]  = "JOIN `##name` AS {$attr}b ON ({$attr}b.n_file={$sql_col_prefix}file AND n_id={$sql_col_prefix}id)";
					$sql_where[] = "{$attr}a.l_type=IN ('HUSB, 'WIFE')";
					$sql_where[] = "{$attr}.n_full LIKE " . WT_DB::quote("%{$match[1]}%");
					if ($sortby == "NAME") {
						$sortby         = "";
						$sql_order_by[] = "{$attr}.n_sort";
					}
					unset($attrs[$attr]); // This filter has been fully processed
				} elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) {
					$sql_join[]  = "JOIN `##places` AS {$attr}a ON ({$attr}a.p_file={$sql_col_prefix}file)";
					$sql_join[]  = "JOIN `##placelinks` AS {$attr}b ON ({$attr}a.p_file={$attr}b.pl_file AND {$attr}b.pl_p_id={$attr}a.p_id AND {$attr}b.pl_gid={$sql_col_prefix}id)";
					$sql_where[] = "{$attr}a.p_place LIKE " . WT_DB::quote("%{$match[1]}%");
					// Don't unset this filter. This is just the first primary PLAC filter to reduce the returned list from the DB
				} /**
				 * General Purpose DB Filter for Individual and Family Lists
				 * Place any other filter before these filters because they will pick up any filters that has not been processed
				 * Also, do not unset() these two filters. These are just the first primary filters to reduce the returned list from the DB
				 */ elseif ($listname == "individual" && preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) {
					$query = "";
					// Level 1 tag
					if ($match[1] != "") {
						$query .= "%1 {$match[1]}%";
					}
					// Level 2 tag
					if ($match[2] != "") {
						$query .= "%2 {$match[2]}%";
					}
					// Contains what?
					if ($match[3] != "") {
						$query .= "%{$match[3]}%";
					}
					$sql_where[] = "i_gedcom LIKE " . WT_DB::quote($query);
				} elseif ($listname == "family" && preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) {
					$query = "";
					// Level 1 tag
					if ($match[1] != "") {
						$query .= "%1 {$match[1]}%";
					}
					// Level 2 tag
					if ($match[2] != "") {
						$query .= "%2 {$match[2]}%";
					}
					// Contains what?
					if ($match[3] != "") {
						$query .= "%{$match[3]}%";
					}
					$sql_where[] = "f_gedcom LIKE " . WT_DB::quote($query);
				} else {
					// TODO: what other filters can we apply in SQL?
				}
			}
		}
		if ($listname == "family") {
			$list = search_fams_custom($sql_join, $sql_where, $sql_order_by);
		} else {
			$list = search_indis_custom($sql_join, $sql_where, $sql_order_by);
		}
		// Clean up the SQL queries - they will not be used again
		unset($sql_join, $sql_where, $sql_order_by);
		break;
	default:
		die("Invalid list name: $listname");
	}

	$filters  = array();
	$filters2 = array();
	if ((isset($attrs['filter1'])) and (count($list) > 0)) {
		foreach ($attrs as $key => $value) {
			if (preg_match("/filter(\d)/", $key)) {
				$condition = $value;
				if (preg_match("/@(\w+)/", $condition, $match)) {
					$id    = $match[1];
					$value = "''";
					if ($id == "ID") {
						if (preg_match("/0 @(.+)@/", $gedrec, $match)) {
							$value = "'" . $match[1] . "'";
						}
					} elseif ($id == "fact") {
						$value = "'$fact'";
					} elseif ($id == "desc") {
						$value = "'$desc'";
					} else {
						if (preg_match("/\d $id (.+)/", $gedrec, $match)) {
							$value = "'" . str_replace("@", "", trim($match[1])) . "'";
						}
					}
					$condition = preg_replace("/@$id/", $value, $condition);
				}
				//-- handle regular expressions
				if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
					$tag  = trim($match[1]);
					$expr = trim($match[2]);
					$val  = trim($match[3]);
					if (preg_match("/\\$(\w+)/", $val, $match)) {
						$val = $vars[$match[1]]['id'];
						$val = trim($val);
					}
					if ($val) {
						$searchstr = "";
						$tags      = explode(":", $tag);
						//-- only limit to a level number if we are specifically looking at a level
						if (count($tags) > 1) {
							$level = 1;
							foreach ($tags as $t) {
								if (!empty($searchstr)) {
									$searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
								}
								//-- search for both EMAIL and _EMAIL... silly double gedcom standard
								if ($t == "EMAIL" || $t == "_EMAIL") {
									$t = "_?EMAIL";
								}
								$searchstr .= $level . " " . $t;
								$level++;
							}
						} else {
							if ($tag == "EMAIL" || $tag == "_EMAIL") {
								$tag = "_?EMAIL";
							}
							$t         = $tag;
							$searchstr = "1 " . $tag;
						}
						switch ($expr) {
						case "CONTAINS":
							if ($t == "PLAC") {
								$searchstr .= "[^\n]*[, ]*" . $val;
							} else {
								$searchstr .= "[^\n]*" . $val;
							}
							$filters[] = $searchstr;
							break;
						default:
							$filters2[] = array("tag" => $tag, "expr" => $expr, "val" => $val);
							break;
						}
					}
				}
			}
		}
	}
	//-- apply other filters to the list that could not be added to the search string
	if ($filters) {
		foreach ($list as $key => $record) {
			foreach ($filters as $filter) {
				if (!preg_match("/" . $filter . "/i", $record->privatizeGedcom(WT_USER_ACCESS_LEVEL))) {
					unset($list[$key]);
					break;
				}
			}
		}
	}
	if ($filters2) {
		$mylist = array();
		foreach ($list as $indi) {
			$key  = $indi->getXref();
			$grec = $indi->privatizeGedcom(WT_USER_ACCESS_LEVEL);
			$keep = true;
			foreach ($filters2 as $filter) {
				if ($keep) {
					$tag  = $filter['tag'];
					$expr = $filter['expr'];
					$val  = $filter['val'];
					if ($val == "''") {
						$val = "";
					}
					$tags = explode(":", $tag);
					$t    = end($tags);
					$v    = get_gedcom_value($tag, 1, $grec);
					//-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
					if ($t == "EMAIL" && empty($v)) {
						$tag  = str_replace("EMAIL", "_EMAIL", $tag);
						$tags = explode(":", $tag);
						$t    = end($tags);
						$v    = get_sub_record(1, $tag, $grec);
					}

					$level = count($tags);
					switch ($expr) {
					case "GTE":
						if ($t == "DATE") {
							$date1 = new WT_Date($v);
							$date2 = new WT_Date($val);
							$keep  = (WT_Date::Compare($date1, $date2) >= 0);
						} elseif ($val >= $v) {
							$keep = true;
						}
						break;
					case "LTE":
						if ($t == "DATE") {
							$date1 = new WT_Date($v);
							$date2 = new WT_Date($val);
							$keep  = (WT_Date::Compare($date1, $date2) <= 0);
						} elseif ($val >= $v) {
							$keep = true;
						}
						break;
					default:
						if ($v == $val) {
							$keep = true;
						} else {
							$keep = false;
						}
						break;
					}
				}
			}
			if ($keep) {
				$mylist[$key] = $indi;
			}
		}
		$list = $mylist;
	}

	switch ($sortby) {
	case "NAME":
		uasort($list, array("WT_GedcomRecord", "Compare"));
		break;
	case "CHAN":
		uasort(
			$list,
			function ($x, $y) {
				$f1 = $x->getFirstFact('CHAN');
				$f2 = $y->getFirstFact('CHAN');
				if ($f1 && $f2) {
					$d1  = $f1->getDate();
					$d2  = $f2->getDate();
					$cmp = WT_Date::compare($d1, $d2);
					if ($cmp) {
						return $cmp;
					} else {
						// Same date.  Compare times
						preg_match('/\n3 TIME (.+)/', $f1->getGedcom(), $m1);
						preg_match('/\n3 TIME (.+)/', $f2->getGedcom(), $m2);

						return strcmp($m1[1], $m2[1]);
					}
				} else {
					return 0;
				}
			}
		);
		break;
	case "BIRT:DATE":
		uasort($list, array("WT_Individual", "CompareBirtDate"));
		break;
	case "DEAT:DATE":
		uasort($list, array("WT_Individual", "CompareDeatDate"));
		break;
	case "MARR:DATE":
		uasort($list, array("WT_Family", "compareMarrDate"));
		break;
	default:
		// unsorted or already sorted by SQL
		break;
	}

	array_push($repeatsStack, array($repeats, $repeatBytes));
	$repeatBytes = xml_get_current_line_number($parser) + 1;
}

/**
 * XML <List> end element handler
 *
 * @see ListSHandler()
 */
function ListEHandler() {
	global $list, $repeats, $repeatsStack, $repeatBytes, $parser, $parserStack, $report, $gedrec, $processRepeats, $list_total, $list_private;

	$processRepeats--;
	if ($processRepeats > 0) {
		return;
	}

	// Check if there is any list
	if (count($list) > 0) {
		// @deprecated
		//$line = xml_get_current_line_number($parser)-1;
		$lineoffset = 0;
		foreach ($repeatsStack as $rep) {
			$lineoffset += $rep[1];
		}
		//-- read the xml from the file
		$lines = file($report);
		while ((strpos($lines[$lineoffset + $repeatBytes], "<List") === false) && (($lineoffset + $repeatBytes) > 0)) {
			$lineoffset--;
		}
		$lineoffset++;
		$reportxml = "<tempdoc>\n";
		$line_nr   = $lineoffset + $repeatBytes;
		// List Level counter
		$count = 1;
		while (0 < $count) {
			if (strpos($lines[$line_nr], "<List") !== false) {
				$count++;
			} elseif (strpos($lines[$line_nr], "</List") !== false) {
				$count--;
			}
			if (0 < $count) {
				$reportxml .= $lines[$line_nr];
			}
			$line_nr++;
		}
		// No need to drag this
		unset($lines);
		$reportxml .= "</tempdoc>";
		// Save original values
		array_push($parserStack, $parser);
		$oldgedrec = $gedrec;

		$list_total   = count($list);
		$list_private = 0;
		foreach ($list as $record) {
			if ($record->canShow()) {
				$gedrec = $record->privatizeGedcom(WT_USER_ACCESS_LEVEL);
				//-- start the sax parser
				$repeat_parser = xml_parser_create();
				$parser        = $repeat_parser;
				//-- make sure everything is case sensitive
				xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
				//-- set the main element handler functions
				xml_set_element_handler($repeat_parser, "startElement", "endElement");
				//-- set the character data handler
				xml_set_character_data_handler($repeat_parser, "characterData");
				if (!xml_parse($repeat_parser, $reportxml, true)) {
					printf(
						$reportxml . "\nListEHandler XML error: %s at line %d",
						xml_error_string(xml_get_error_code($repeat_parser)),
						xml_get_current_line_number($repeat_parser)
					);
					exit;
				}
				xml_parser_free($repeat_parser);
			} else {
				$list_private++;
			}
		}
		// Clean up the GLOBAL list array
		unset($list);
		$parser = array_pop($parserStack);
		$gedrec = $oldgedrec;
	}
	$temp        = array_pop($repeatsStack);
	$repeats     = $temp[0];
	$repeatBytes = $temp[1];
}

/**
 * XML <ListTotal> element handler
 *
 * Prints the total number of records in a list
 * The total number is collected from
 * List and Relatives
 */
function ListTotalSHandler() {
	global $list_total, $list_private, $currentElement;

	if (empty($list_total)) {
		$list_total = 0;
	}

	if ($list_private == 0) {
		$currentElement->addText($list_total);
	} else {
		$currentElement->addText(($list_total - $list_private) . " / " . $list_total);
	}
}

/**
 * @param array $attrs an array of key value pairs for the attributes
 *
 * @see  RelativesEHandler()
 */
function RelativesSHandler($attrs) {
	global $repeats, $repeatBytes, $list, $repeatsStack, $processRepeats, $parser, $vars, $sortby;

	$processRepeats++;
	if ($processRepeats > 1) {
		return;
	}

	$sortby = "NAME";
	if (isset($attrs['sortby'])) {
		$sortby = $attrs['sortby'];
	}
	$match = array();
	if (preg_match("/\\$(\w+)/", $sortby, $match)) {
		$sortby = $vars[$match[1]]['id'];
		$sortby = trim($sortby);
	}

	$maxgen = -1;
	if (isset($attrs['maxgen'])) {
		$maxgen = $attrs['maxgen'];
	}
	if ($maxgen == "*") {
		$maxgen = -1;
	}

	$group = "child-family";
	if (isset($attrs['group'])) {
		$group = $attrs['group'];
	}
	if (preg_match("/\\$(\w+)/", $group, $match)) {
		$group = $vars[$match[1]]['id'];
		$group = trim($group);
	}

	$id = "";
	if (isset($attrs['id'])) {
		$id = $attrs['id'];
	}
	if (preg_match("/\\$(\w+)/", $id, $match)) {
		$id = $vars[$match[1]]['id'];
		$id = trim($id);
	}

	$list   = array();
	$person = WT_Individual::getInstance($id);
	if (!empty($person)) {
		$list[$id] = $person;
		switch ($group) {
		case "child-family":
			foreach ($person->getChildFamilies() as $family) {
				$husband = $family->getHusband();
				$wife    = $family->getWife();
				if (!empty($husband)) {
					$list[$husband->getXref()] = $husband;
				}
				if (!empty($wife)) {
					$list[$wife->getXref()] = $wife;
				}
				$children = $family->getChildren();
				foreach ($children as $child) {
					if (!empty($child)) {
						$list[$child->getXref()] = $child;
					}
				}
			}
			break;
		case "spouse-family":
			foreach ($person->getSpouseFamilies() as $family) {
				$husband = $family->getHusband();
				$wife    = $family->getWife();
				if (!empty($husband)) {
					$list[$husband->getXref()] = $husband;
				}
				if (!empty($wife)) {
					$list[$wife->getXref()] = $wife;
				}
				$children = $family->getChildren();
				foreach ($children as $child) {
					if (!empty($child)) {
						$list[$child->getXref()] = $child;
					}
				}
			}
			break;
		case "direct-ancestors":
			add_ancestors($list, $id, false, $maxgen);
			break;
		case "ancestors":
			add_ancestors($list, $id, true, $maxgen);
			break;
		case "descendants":
			$list[$id]->generation = 1;
			add_descendancy($list, $id, false, $maxgen);
			break;
		case "all":
			add_ancestors($list, $id, true, $maxgen);
			add_descendancy($list, $id, true, $maxgen);
			break;
		}
	}

	switch ($sortby) {
	case "NAME":
		uasort($list, array("WT_GedcomRecord", "Compare"));
		break;
	case "BIRT:DATE":
		uasort($list, array("WT_Individual", "CompareBirtDate"));
		break;
	case "DEAT:DATE":
		uasort($list, array("WT_Individual", "CompareDeatDate"));
		break;
	case "generation":
		$newarray = array();
		reset($list);
		$genCounter = 1;
		while (count($newarray) < count($list)) {
			foreach ($list as $key => $value) {
				$generation = $value->generation;
				if ($generation == $genCounter) {
					$newarray[$key]             = new stdClass();
					$newarray[$key]->generation = $generation;
				}
			}
			$genCounter++;
		}
		$list = $newarray;
		break;
	default:
		// unsorted
		break;
	}
	array_push($repeatsStack, array($repeats, $repeatBytes));
	$repeatBytes = xml_get_current_line_number($parser) + 1;
}

/**
 * XML </ Relatives> end element handler
 *
 * @see RelativesSHandler()
 */
function RelativesEHandler() {
	global $list, $repeats, $repeatsStack, $repeatBytes, $parser, $parserStack, $report, $gedrec, $processRepeats, $list_total, $list_private, $generation;

	$processRepeats--;
	if ($processRepeats > 0) {
		return;
	}

	// Check if there is any relatives
	if (count($list) > 0) {

		// @deprecated
		//$line = xml_get_current_line_number($parser)-1;
		$lineoffset = 0;
		foreach ($repeatsStack as $rep) {
			$lineoffset += $rep[1];
		}
		//-- read the xml from the file
		$lines = file($report);
		while ((strpos($lines[$lineoffset + $repeatBytes], "<Relatives") === false) && (($lineoffset + $repeatBytes) > 0)) {
			$lineoffset--;
		}
		$lineoffset++;
		$reportxml = "<tempdoc>\n";
		$line_nr   = $lineoffset + $repeatBytes;
		// Relatives Level counter
		$count = 1;
		while (0 < $count) {
			if (strpos($lines[$line_nr], "<Relatives") !== false) {
				$count++;
			} elseif (strpos($lines[$line_nr], "</Relatives") !== false) {
				$count--;
			}
			if (0 < $count) {
				$reportxml .= $lines[$line_nr];
			}
			$line_nr++;
		}
		// No need to drag this
		unset($lines);
		$reportxml .= "</tempdoc>\n";
		// Save original values
		array_push($parserStack, $parser);
		$oldgedrec = $gedrec;

		$list_total   = count($list);
		$list_private = 0;
		foreach ($list as $key => $value) {
			if (isset($value->generation)) {
				$generation = $value->generation;
			}
			$tmp    = WT_GedcomRecord::getInstance($key);
			$gedrec = $tmp->privatizeGedcom(WT_USER_ACCESS_LEVEL);
			//-- start the sax parser
			$repeat_parser = xml_parser_create();
			$parser        = $repeat_parser;
			//-- make sure everything is case sensitive
			xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
			//-- set the main element handler functions
			xml_set_element_handler($repeat_parser, "startElement", "endElement");
			//-- set the character data handler
			xml_set_character_data_handler($repeat_parser, "characterData");

			if (!xml_parse($repeat_parser, $reportxml, true)) {
				printf(
					$reportxml . "\nRelativesEHandler XML error: %s at line %d",
					xml_error_string(xml_get_error_code($repeat_parser)),
					xml_get_current_line_number($repeat_parser)
				);
				exit;
			}
			xml_parser_free($repeat_parser);
		}
		// Clean up the GLOBAL list array
		unset($list);
		$parser = array_pop($parserStack);
		$gedrec = $oldgedrec;
	}
	$temp        = array_pop($repeatsStack);
	$repeats     = $temp[0];
	$repeatBytes = $temp[1];
}

/**
 * XML <Generation /> element handler
 *
 * Prints the number of generations
 *
 * @see  Element::addText()
 */
function GenerationSHandler() {
	global $generation, $currentElement;

	if (empty($generation)) {
		$generation = 1;
	}

	$currentElement->addText($generation);
}

/**
 * XML <NewPage /> element handler
 *
 * Has to be placed in an element (header, pageheader, body or footer)
 */
function NewPageSHandler() {
	global $wt_report;

	$temp = "addpage";
	$wt_report->addElement($temp);
}

/**
 * @param array  $attrs an array of key value pairs for the attributes
 * @param string $tag   HTML tag name
 *
 * @see  HTMLEHandler()
 * @see  WT_Report_Base::createHTML()
 */
function HTMLSHandler($tag, $attrs) {
	global $printData, $printDataStack, $wt_reportStack, $wt_report, $currentElement, $ReportRoot;

	if ($tag == "tempdoc") {
		return;
	}
	array_push($wt_reportStack, $wt_report);
	$wt_report      = $ReportRoot->createHTML($tag, $attrs);
	$currentElement = $wt_report;

	array_push($printDataStack, $printData);
	$printData = true;
}

/**
 * @param $tag
 */
function HTMLEHandler($tag) {
	global $printData, $printDataStack, $wt_report, $currentElement, $wt_reportStack;
	if ($tag == "tempdoc") {
		return;
	}

	$printData      = array_pop($printDataStack);
	$currentElement = $wt_report;
	$wt_report      = array_pop($wt_reportStack);
	if (!is_null($wt_report)) {
		$wt_report->addElement($currentElement);
	} else {
		$wt_report = $currentElement;
	}
}

/**
 * XML <TitleSHandler> start element handler
 *
 * @see  TitleEHandler()
 */
function TitleSHandler() {
	global $reportTitle;
	$reportTitle = true;
}

/**
 * XML </TitleEHandler> end element handler
 *
 * @see TitleSHandler()
 * @final
 */
function TitleEHandler() {
	global $reportTitle;
	$reportTitle = false;
}

/**
 * XML <DescriptionSHandler> start element handler
 */
function DescriptionSHandler() {
	global $reportDescription;
	$reportDescription = true;
}

/**
 * XML </DescriptionEHandler> end element handler
 *
 * @see DescriptionSHandler()
 * @final
 */
function DescriptionEHandler() {
	global $reportDescription;
	$reportDescription = false;
}

/**
 * get gedcom tag value
 *
 * @param string $tag    The tag to find, use : to delineate subtags
 * @param int    $level  The gedcom line level of the first tag to find, setting level to 0 will cause it to use 1+ the level of the incoming record
 * @param string $gedrec The gedcom record to get the value from
 *
 * @return string the value of a gedcom tag from the given gedcom record
 */
function get_gedcom_value($tag, $level, $gedrec) {
	if (empty($gedrec)) {
		return '';
	}
	$tags      = explode(':', $tag);
	$origlevel = $level;
	if ($level == 0) {
		$level = $gedrec{0} + 1;
	}

	$subrec = $gedrec;
	foreach ($tags as $t) {
		$lastsubrec = $subrec;
		$subrec     = get_sub_record($level, "$level $t", $subrec);
		if (empty($subrec) && $origlevel == 0) {
			$level--;
			$subrec = get_sub_record($level, "$level $t", $lastsubrec);
		}
		if (empty($subrec)) {
			if ($t == "TITL") {
				$subrec = get_sub_record($level, "$level ABBR", $lastsubrec);
				if (!empty($subrec)) {
					$t = "ABBR";
				}
			}
			if (empty($subrec)) {
				if ($level > 0) {
					$level--;
				}
				$subrec = get_sub_record($level, "@ $t", $gedrec);
				if (empty($subrec)) {
					return;
				}
			}
		}
		$level++;
	}
	$level--;
	$ct = preg_match("/$level $t(.*)/", $subrec, $match);
	if ($ct == 0) {
		$ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
	}
	if ($ct == 0) {
		$ct = preg_match("/@ $t (.+)/", $subrec, $match);
	}
	if ($ct > 0) {
		$value = trim($match[1]);
		if ($t == 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
			$note = WT_Note::getInstance($match[1]);
			if ($note) {
				$value = $note->getNote();
			} else {
				//-- set the value to the id without the @
				$value = $match[1];
			}
		}
		if ($level != 0 || $t != "NOTE") {
			$value .= get_cont($level + 1, $subrec);
		}

		return $value;
	}

	return "";
}

function add_ancestors(&$list, $pid, $children = false, $generations = -1, $show_empty = false) {
	$genlist                = array($pid);
	$list[$pid]->generation = 1;
	while (count($genlist) > 0) {
		$id = array_shift($genlist);
		if (strpos($id, 'empty') === 0) {
			continue; // id can be something like “empty7”
		}
		$person = WT_Individual::getInstance($id);
		foreach ($person->getChildFamilies() as $family) {
			$husband = $family->getHusband();
			$wife    = $family->getWife();
			if ($husband) {
				$list[$husband->getXref()]             = $husband;
				$list[$husband->getXref()]->generation = $list[$id]->generation + 1;
			}
			if ($wife) {
				$list[$wife->getXref()]             = $wife;
				$list[$wife->getXref()]->generation = $list[$id]->generation + 1;
			}
			if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
				if ($husband) {
					array_push($genlist, $husband->getXref());
				}
				if ($wife) {
					array_push($genlist, $wife->getXref());
				}
			}
			if ($children) {
				foreach ($family->getChildren() as $child) {
					$list[$child->getXref()] = $child;
					if (isset($list[$id]->generation)) {
						$list[$child->getXref()]->generation = $list[$id]->generation;
					} else {
						$list[$child->getXref()]->generation = 1;
					}
				}
			}
		}
	}
}

function add_descendancy(&$list, $pid, $parents = false, $generations = -1) {
	$person = WT_Individual::getInstance($pid);
	if ($person == null) {
		return;
	}
	if (!isset($list[$pid])) {
		$list[$pid] = $person;
	}
	if (!isset($list[$pid]->generation)) {
		$list[$pid]->generation = 0;
	}
	foreach ($person->getSpouseFamilies() as $family) {
		if ($parents) {
			$husband = $family->getHusband();
			$wife    = $family->getWife();
			if ($husband) {
				$list[$husband->getXref()] = $husband;
				if (isset($list[$pid]->generation)) {
					$list[$husband->getXref()]->generation = $list[$pid]->generation - 1;
				} else {
					$list[$husband->getXref()]->generation = 1;
				}
			}
			if ($wife) {
				$list[$wife->getXref()] = $wife;
				if (isset($list[$pid]->generation)) {
					$list[$wife->getXref()]->generation = $list[$pid]->generation - 1;
				} else {
					$list[$wife->getXref()]->generation = 1;
				}
			}
		}
		$children = $family->getChildren();
		foreach ($children as $child) {
			if ($child) {
				$list[$child->getXref()] = $child;
				if (isset($list[$pid]->generation)) {
					$list[$child->getXref()]->generation = $list[$pid]->generation + 1;
				} else {
					$list[$child->getXref()]->generation = 2;
				}
			}
		}
		if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
			foreach ($children as $child) {
				add_descendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family
			}
		}
	}
}
