Completed
Push — 1.7 ( fa0ef8...580c36 )
by Greg
08:51
created

RelationshipController::calculateRelationships()   C

Complexity

Conditions 13
Paths 72

Size

Total Lines 69
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 43
nc 72
nop 4
dl 0
loc 69
rs 5.6955
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2016 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Controller;
17
18
use Fisharebest\Algorithm\Dijkstra;
19
use Fisharebest\Webtrees\Database;
20
use Fisharebest\Webtrees\Family;
21
use Fisharebest\Webtrees\GedcomRecord;
22
use Fisharebest\Webtrees\Individual;
23
24
/**
25
 * Controller for the relationships calculations
26
 */
27
class RelationshipController extends PageController {
28
	/**
29
	 * Calculate the shortest paths - or all paths - between two individuals.
30
	 *
31
	 * @param Individual $individual1
32
	 * @param Individual $individual2
33
	 * @param int        $recursion   How many levels of recursion to use
34
	 * @param boo;       $ancestor    Restrict to relationships via a common ancestor
35
	 *
36
	 * @return string[][]
37
	 */
38
	public function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false) {
39
40
		$rows = Database::prepare(
41
			"SELECT l_from, l_to FROM `##link` WHERE l_file = :tree_id AND l_type IN ('FAMS', 'FAMC')"
42
		)->execute(array(
43
			'tree_id' => $individual1->getTree()->getTreeId(),
44
		))->fetchAll();
45
46
		// Optionally restrict the graph to the ancestors of the individuals.
47
		if ($ancestor) {
48
			$ancestors = $this->allAncestors($individual1->getXref(), $individual2->getXref(), $individual1->getTree()->getTreeId());
49
			$exclude   = $this->excludeFamilies($individual1->getXref(), $individual2->getXref(), $individual1->getTree()->getTreeId());
50
		} else {
51
			$ancestors = array();
52
			$exclude   = array();
53
		}
54
55
		$graph = array();
56
57
		foreach ($rows as $row) {
58
			if (!$ancestors || in_array($row->l_from, $ancestors) && !in_array($row->l_to, $exclude)) {
59
				$graph[$row->l_from][$row->l_to] = 1;
60
				$graph[$row->l_to][$row->l_from] = 1;
61
			}
62
		}
63
64
		$xref1    = $individual1->getXref();
65
		$xref2    = $individual2->getXref();
66
		$dijkstra = new Dijkstra($graph);
67
		$paths    = $dijkstra->shortestPaths($xref1, $xref2);
68
69
		// Only process each exclusion list once;
70
		$excluded = array();
71
72
		$queue = array();
73
		foreach ($paths as $path) {
74
			// Insert the paths into the queue, with an exclusion list.
75
			$queue[] = array('path' => $path, 'exclude' => array());
76
			// While there are un-extended paths
77
			while (list(, $next) = each($queue)) {
78
				// For each family on the path
79
				for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) {
80
					$exclude = $next['exclude'];
81
					if (count($exclude) >= $recursion) {
82
						continue;
83
					}
84
					$exclude[] = $next['path'][$n];
85
					sort($exclude);
86
					$tmp = implode('-', $exclude);
87
					if (in_array($tmp, $excluded)) {
88
						continue;
89
					} else {
90
						$excluded[] = $tmp;
91
					}
92
					// Add any new path to the queue
93
					foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) {
94
						$queue[] = array('path' => $new_path, 'exclude' => $exclude);
95
					}
96
				}
97
			}
98
		}
99
		// Extract the paths from the queue, removing duplicates.
100
		$paths = array();
101
		foreach ($queue as $next) {
102
			$paths[implode('-', $next['path'])] = $next['path'];
103
		}
104
105
		return $paths;
106
	}
107
108
	/**
109
	 * Convert a path (list of XREFs) to an "old-style" string of relationships.
110
	 *
111
	 * Return an empty array, if privacy rules prevent us viewing any node.
112
	 *
113
	 * @param GedcomRecord[] $path Alternately Individual / Family
114
	 *
115
	 * @return string[]
116
	 */
117
	public function oldStyleRelationshipPath(array $path) {
118
		global $WT_TREE;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
119
120
		$spouse_codes  = array('M' => 'hus', 'F' => 'wif', 'U' => 'spo');
121
		$parent_codes  = array('M' => 'fat', 'F' => 'mot', 'U' => 'par');
122
		$child_codes   = array('M' => 'son', 'F' => 'dau', 'U' => 'chi');
123
		$sibling_codes = array('M' => 'bro', 'F' => 'sis', 'U' => 'sib');
124
		$relationships = array();
125
126
		for ($i = 1; $i < count($path); $i += 2) {
127
			$family = Family::getInstance($path[$i], $WT_TREE);
128
			$prev   = Individual::getInstance($path[$i - 1], $WT_TREE);
129
			$next   = Individual::getInstance($path[$i + 1], $WT_TREE);
130
			if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->getXref() . '@/', $family->getGedcom(), $match)) {
131
				$rel1 = $match[1];
132
			} else {
133
				return array();
134
			}
135
			if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->getXref() . '@/', $family->getGedcom(), $match)) {
136
				$rel2 = $match[1];
137
			} else {
138
				return array();
139
			}
140
			if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
141
				$relationships[$i] = $spouse_codes[$next->getSex()];
142
			} elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') {
143
				$relationships[$i] = $child_codes[$next->getSex()];
144
			} elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
145
				$relationships[$i] = $parent_codes[$next->getSex()];
146
			} elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') {
147
				$relationships[$i] = $sibling_codes[$next->getSex()];
148
			}
149
		}
150
151
		return $relationships;
152
	}
153
154
	/**
155
	 * Find all ancestors of a list of individuals
156
	 *
157
	 * @param string $xref1
158
	 * @param string $xref2
159
	 * @param int    $tree_id
160
	 *
161
	 * @return array
162
	 */
163
	private function allAncestors($xref1, $xref2, $tree_id) {
164
		$ancestors = array($xref1, $xref2);
165
166
		$queue = array($xref1, $xref2);
167
		while (!empty($queue)) {
168
			$placeholders = implode(',', array_fill(0, count($queue), '?'));
169
			$parameters = $queue;
170
			$parameters[] = $tree_id;
171
172
			$parents = Database::prepare(
173
				"SELECT l2.l_from" .
174
				" FROM `##link` AS l1" .
175
				" JOIN `##link` AS l2 USING (l_to, l_file) " .
176
				" WHERE l1.l_type = 'FAMC' AND l2.l_type = 'FAMS' AND l1.l_from IN (" . $placeholders . ") AND l_file = ?"
177
			)->execute(
178
				$parameters
179
			)->fetchOneColumn();
180
181
			$queue = array()
182
			foreach ($parents as $parent) {
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_FOREACH
Loading history...
183
				if (!in_array($parent, $ancestors)) {
184
					$ancestors[] = $parent;
185
					$queue[]     = $parent;
186
				}
187
			}
188
		}
189
190
		return $ancestors;
191
	}
192
193
	/**
194
	 * Find all families of two individuals
195
	 *
196
	 * @param string $xref1
197
	 * @param string $xref2
198
	 * @param int    $tree_id
199
	 *
200
	 * @return array
201
	 */
202
	private function excludeFamilies($xref1, $xref2, $tree_id) {
203
		return Database::prepare(
204
			"SELECT l_to" .
205
			" FROM `##link` AS l1" .
206
			" JOIN `##link` AS l2 USING (l_type, l_to, l_file) " .
207
			" WHERE l_type = 'FAMS' AND l1.l_from = :spouse1 AND l2.l_from = :spouse2 AND l_file = :tree_id"
208
		)->execute(array(
209
			'spouse1' => $xref1,
210
			'spouse2' => $xref2,
211
			'tree_id' => $tree_id,
212
		))->fetchOneColumn();
213
	}
214
}
215