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