1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2018 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees; 17 18/** 19 * A GEDCOM place (PLAC) object. 20 */ 21class Place 22{ 23 const GEDCOM_SEPARATOR = ', '; 24 25 /** @var string[] e.g. array('Westminster', 'London', 'England') */ 26 private $gedcom_place; 27 28 /** @var Tree We may have the same place name in different trees. */ 29 private $tree; 30 31 /** 32 * Create a place. 33 * 34 * @param string $gedcom_place 35 * @param Tree $tree 36 */ 37 public function __construct($gedcom_place, Tree $tree) 38 { 39 if ($gedcom_place === '') { 40 $this->gedcom_place = []; 41 } else { 42 $this->gedcom_place = explode(self::GEDCOM_SEPARATOR, $gedcom_place); 43 } 44 $this->tree = $tree; 45 } 46 47 /** 48 * Extract the country (last part) of a place name. 49 * 50 * @return string - e.g. "England" 51 */ 52 public function lastPart(): string 53 { 54 return end($this->gedcom_place); 55 } 56 57 /** 58 * Get the identifier for a place. 59 * 60 * @return int 61 * @throws \Exception 62 */ 63 public function getPlaceId(): int 64 { 65 $place_id = 0; 66 foreach (array_reverse($this->gedcom_place) as $place) { 67 $place_id = Database::prepare( 68 "SELECT p_id FROM `##places` WHERE p_parent_id = :parent_id AND p_place = :place AND p_file = :tree_id" 69 )->execute([ 70 'parent_id' => $place_id, 71 'place' => $place, 72 'tree_id' => $this->tree->getTreeId(), 73 ])->fetchOne(); 74 } 75 76 return $place_id; 77 } 78 79 /** 80 * Get the higher level place. 81 * 82 * @return Place 83 */ 84 public function getParentPlace(): Place 85 { 86 return new self(implode(self::GEDCOM_SEPARATOR, array_slice($this->gedcom_place, 1)), $this->tree); 87 } 88 89 /** 90 * Get the lower level places. 91 * 92 * @return Place[] 93 * @throws \Exception 94 */ 95 public function getChildPlaces(): array 96 { 97 $children = []; 98 if ($this->getPlaceId()) { 99 $parent_text = self::GEDCOM_SEPARATOR . $this->getGedcomName(); 100 } else { 101 $parent_text = ''; 102 } 103 104 $rows = Database::prepare( 105 "SELECT p_place FROM `##places`" . 106 " WHERE p_parent_id = :parent_id AND p_file = :tree_id" . 107 " ORDER BY p_place COLLATE :collation" 108 )->execute([ 109 'parent_id' => $this->getPlaceId(), 110 'tree_id' => $this->tree->getTreeId(), 111 'collation' => I18N::collation(), 112 ])->fetchOneColumn(); 113 foreach ($rows as $row) { 114 $children[] = new self($row . $parent_text, $this->tree); 115 } 116 117 return $children; 118 } 119 120 /** 121 * Create a URL to the place-hierarchy page. 122 * 123 * @return string 124 */ 125 public function getURL(): string 126 { 127 return e(route('place-hierarchy', [ 128 'parent' => array_reverse($this->gedcom_place), 129 'ged' => $this->tree->getName(), 130 ])); 131 } 132 133 /** 134 * Format this name for GEDCOM data. 135 * 136 * @return string 137 */ 138 public function getGedcomName(): string 139 { 140 return implode(self::GEDCOM_SEPARATOR, $this->gedcom_place); 141 } 142 143 /** 144 * Format this place for display on screen. 145 * 146 * @return string 147 */ 148 public function getPlaceName(): string 149 { 150 $place = reset($this->gedcom_place); 151 152 return $place ? '<span dir="auto">' . e($place) . '</span>' : I18N::translate('unknown'); 153 } 154 155 /** 156 * Is this a null/empty/missing/invalid place? 157 * 158 * @return bool 159 */ 160 public function isEmpty(): bool 161 { 162 return empty($this->gedcom_place); 163 } 164 165 /** 166 * Generate the place name for display, including the full hierarchy. 167 * 168 * @return string 169 */ 170 public function getFullName() 171 { 172 if (true) { 173 // If a place hierarchy is a single entity 174 return '<span dir="auto">' . e(implode(I18N::$list_separator, $this->gedcom_place)) . '</span>'; 175 } else { 176 // If a place hierarchy is a list of distinct items 177 $tmp = []; 178 foreach ($this->gedcom_place as $place) { 179 $tmp[] = '<span dir="auto">' . e($place) . '</span>'; 180 } 181 182 return implode(I18N::$list_separator, $tmp); 183 } 184 } 185 186 /** 187 * For lists and charts, where the full name won’t fit. 188 * 189 * @return string 190 */ 191 public function getShortName() 192 { 193 $SHOW_PEDIGREE_PLACES = (int)$this->tree->getPreference('SHOW_PEDIGREE_PLACES'); 194 195 if ($SHOW_PEDIGREE_PLACES >= count($this->gedcom_place)) { 196 // A short place name - no need to abbreviate 197 return $this->getFullName(); 198 } else { 199 // Abbreviate the place name, for lists 200 if ($this->tree->getPreference('SHOW_PEDIGREE_PLACES_SUFFIX')) { 201 // The *last* $SHOW_PEDIGREE_PLACES components 202 $short_name = implode(self::GEDCOM_SEPARATOR, array_slice($this->gedcom_place, -$SHOW_PEDIGREE_PLACES)); 203 } else { 204 // The *first* $SHOW_PEDIGREE_PLACES components 205 $short_name = implode(self::GEDCOM_SEPARATOR, array_slice($this->gedcom_place, 0, $SHOW_PEDIGREE_PLACES)); 206 } 207 208 // Add a tool-tip showing the full name 209 return '<span title="' . e($this->getGedcomName()) . '" dir="auto">' . e($short_name) . '</span>'; 210 } 211 } 212 213 /** 214 * For the Place hierarchy "list all" option 215 * 216 * @return string 217 */ 218 public function getReverseName(): string 219 { 220 $tmp = []; 221 foreach (array_reverse($this->gedcom_place) as $place) { 222 $tmp[] = '<span dir="auto">' . e($place) . '</span>'; 223 } 224 225 return implode(I18N::$list_separator, $tmp); 226 } 227 228 /** 229 * Fetch all places from the database. 230 * 231 * @param Tree $tree 232 * 233 * @return Place[] 234 * @throws \Exception 235 */ 236 public static function allPlaces(Tree $tree): array 237 { 238 $places = []; 239 $rows = 240 Database::prepare( 241 "SELECT CONCAT_WS(', ', p1.p_place, p2.p_place, p3.p_place, p4.p_place, p5.p_place, p6.p_place, p7.p_place, p8.p_place, p9.p_place)" . 242 " FROM `##places` AS p1" . 243 " LEFT JOIN `##places` AS p2 ON (p1.p_parent_id = p2.p_id)" . 244 " LEFT JOIN `##places` AS p3 ON (p2.p_parent_id = p3.p_id)" . 245 " LEFT JOIN `##places` AS p4 ON (p3.p_parent_id = p4.p_id)" . 246 " LEFT JOIN `##places` AS p5 ON (p4.p_parent_id = p5.p_id)" . 247 " LEFT JOIN `##places` AS p6 ON (p5.p_parent_id = p6.p_id)" . 248 " LEFT JOIN `##places` AS p7 ON (p6.p_parent_id = p7.p_id)" . 249 " LEFT JOIN `##places` AS p8 ON (p7.p_parent_id = p8.p_id)" . 250 " LEFT JOIN `##places` AS p9 ON (p8.p_parent_id = p9.p_id)" . 251 " WHERE p1.p_file = :tree_id" . 252 " ORDER BY CONCAT_WS(', ', p9.p_place, p8.p_place, p7.p_place, p6.p_place, p5.p_place, p4.p_place, p3.p_place, p2.p_place, p1.p_place) COLLATE :collate" 253 ) 254 ->execute([ 255 'tree_id' => $tree->getTreeId(), 256 'collate' => I18N::collation(), 257 ])->fetchOneColumn(); 258 foreach ($rows as $row) { 259 $places[] = new self($row, $tree); 260 } 261 262 return $places; 263 } 264 265 /** 266 * Search for a place name. 267 * 268 * @param string $filter 269 * @param Tree $tree 270 * 271 * @return Place[] 272 * @throws \Exception 273 */ 274 public static function findPlaces($filter, Tree $tree): array 275 { 276 $places = []; 277 $rows = 278 Database::prepare( 279 "SELECT CONCAT_WS(', ', p1.p_place, p2.p_place, p3.p_place, p4.p_place, p5.p_place, p6.p_place, p7.p_place, p8.p_place, p9.p_place)" . 280 " FROM `##places` AS p1" . 281 " LEFT JOIN `##places` AS p2 ON (p1.p_parent_id = p2.p_id)" . 282 " LEFT JOIN `##places` AS p3 ON (p2.p_parent_id = p3.p_id)" . 283 " LEFT JOIN `##places` AS p4 ON (p3.p_parent_id = p4.p_id)" . 284 " LEFT JOIN `##places` AS p5 ON (p4.p_parent_id = p5.p_id)" . 285 " LEFT JOIN `##places` AS p6 ON (p5.p_parent_id = p6.p_id)" . 286 " LEFT JOIN `##places` AS p7 ON (p6.p_parent_id = p7.p_id)" . 287 " LEFT JOIN `##places` AS p8 ON (p7.p_parent_id = p8.p_id)" . 288 " LEFT JOIN `##places` AS p9 ON (p8.p_parent_id = p9.p_id)" . 289 " WHERE CONCAT_WS(', ', p1.p_place, p2.p_place, p3.p_place, p4.p_place, p5.p_place, p6.p_place, p7.p_place, p8.p_place, p9.p_place) LIKE CONCAT('%', :filter_1, '%') AND CONCAT_WS(', ', p1.p_place, p2.p_place, p3.p_place, p4.p_place, p5.p_place, p6.p_place, p7.p_place, p8.p_place, p9.p_place) NOT LIKE CONCAT('%,%', :filter_2, '%') AND p1.p_file = :tree_id" . 290 " ORDER BY CONCAT_WS(', ', p1.p_place, p2.p_place, p3.p_place, p4.p_place, p5.p_place, p6.p_place, p7.p_place, p8.p_place, p9.p_place) COLLATE :collation" 291 )->execute([ 292 'filter_1' => preg_quote($filter), 293 'filter_2' => preg_quote($filter), 294 'tree_id' => $tree->getTreeId(), 295 'collation' => I18N::collation(), 296 ])->fetchOneColumn(); 297 foreach ($rows as $row) { 298 $places[] = new self($row, $tree); 299 } 300 301 return $places; 302 } 303} 304