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