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