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