1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Fisharebest\Webtrees\Module\ModuleInterface; 23use Fisharebest\Webtrees\Module\ModuleListInterface; 24use Fisharebest\Webtrees\Module\PlaceHierarchyListModule; 25use Fisharebest\Webtrees\Services\ModuleService; 26use Illuminate\Database\Capsule\Manager as DB; 27use Illuminate\Database\Query\Expression; 28use Illuminate\Support\Collection; 29 30/** 31 * A GEDCOM place (PLAC) object. 32 */ 33class Place 34{ 35 /** @var string e.g. "Westminster, London, England" */ 36 private $place_name; 37 38 /** @var Collection<string> The parts of a place name, e.g. ["Westminster", "London", "England"] */ 39 private $parts; 40 41 /** @var Tree We may have the same place name in different trees. */ 42 private $tree; 43 44 /** 45 * Create a place. 46 * 47 * @param string $place_name 48 * @param Tree $tree 49 */ 50 public function __construct(string $place_name, Tree $tree) 51 { 52 // Ignore any empty parts in place names such as "Village, , , Country". 53 $this->parts = Collection::make(preg_split(Gedcom::PLACE_SEPARATOR_REGEX, $place_name)) 54 ->filter(); 55 56 // Rebuild the placename in the correct format. 57 $this->place_name = $this->parts->implode(Gedcom::PLACE_SEPARATOR); 58 59 $this->tree = $tree; 60 } 61 62 /** 63 * Get the higher level place. 64 * 65 * @return Place 66 */ 67 public function parent(): Place 68 { 69 return new self($this->parts->slice(1)->implode(Gedcom::PLACE_SEPARATOR), $this->tree); 70 } 71 72 /** 73 * The database row that contains this place. 74 * Note that due to database collation, both "Quebec" and "Québec" will share the same row. 75 * 76 * @return int 77 */ 78 public function id(): int 79 { 80 return app('cache.array')->remember('place-' . $this->place_name, function (): int { 81 // The "top-level" place won't exist in the database. 82 if ($this->parts->isEmpty()) { 83 return 0; 84 } 85 86 $parent_place_id = $this->parent()->id(); 87 88 $place_id = (int) DB::table('places') 89 ->where('p_file', '=', $this->tree->id()) 90 ->where('p_place', '=', $this->parts->first()) 91 ->where('p_parent_id', '=', $parent_place_id) 92 ->value('p_id'); 93 94 if ($place_id === 0) { 95 $place = $this->parts->first(); 96 97 DB::table('places')->insert([ 98 'p_file' => $this->tree->id(), 99 'p_place' => $place, 100 'p_parent_id' => $parent_place_id, 101 'p_std_soundex' => Soundex::russell($place), 102 'p_dm_soundex' => Soundex::daitchMokotoff($place), 103 ]); 104 105 $place_id = (int) DB::connection()->getPdo()->lastInsertId(); 106 } 107 108 return $place_id; 109 }); 110 } 111 112 /** 113 * Extract the locality (first parts) of a place name. 114 * 115 * @param int $n 116 * 117 * @return Collection<string> 118 */ 119 public function firstParts(int $n): Collection 120 { 121 return $this->parts->slice(0, $n); 122 } 123 124 /** 125 * Extract the country (last parts) of a place name. 126 * 127 * @param int $n 128 * 129 * @return Collection<string> 130 */ 131 public function lastParts(int $n): Collection 132 { 133 return $this->parts->slice(-$n); 134 } 135 136 /** 137 * Get the lower level places. 138 * 139 * @return Place[] 140 */ 141 public function getChildPlaces(): array 142 { 143 if ($this->place_name !== '') { 144 $parent_text = Gedcom::PLACE_SEPARATOR . $this->place_name; 145 } else { 146 $parent_text = ''; 147 } 148 149 return DB::table('places') 150 ->where('p_file', '=', $this->tree->id()) 151 ->where('p_parent_id', '=', $this->id()) 152 ->orderBy(new Expression('p_place /*! COLLATE ' . I18N::collation() . ' */')) 153 ->pluck('p_place') 154 ->map(function (string $place) use ($parent_text): Place { 155 return new self($place . $parent_text, $this->tree); 156 }) 157 ->all(); 158 } 159 160 /** 161 * Create a URL to the place-hierarchy page. 162 * 163 * @return string 164 */ 165 public function url(): string 166 { 167 //find a module providing the place hierarchy 168 $module = app(ModuleService::class) 169 ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user()) 170 ->first(static function (ModuleInterface $module): bool { 171 return $module instanceof PlaceHierarchyListModule; 172 }); 173 174 if ($module instanceof PlaceHierarchyListModule) { 175 return $module->listUrl($this->tree, [ 176 'parent' => $this->parts->reverse()->all(), 177 'tree' => $this->tree->name(), 178 ]); 179 } 180 181 // The place-list module is disabled... 182 return '#'; 183 } 184 185 /** 186 * Format this place for GEDCOM data. 187 * 188 * @return string 189 */ 190 public function gedcomName(): string 191 { 192 return $this->place_name; 193 } 194 195 /** 196 * Format this place for display on screen. 197 * 198 * @return string 199 */ 200 public function placeName(): string 201 { 202 $place_name = $this->parts->first() ?? I18N::translate('unknown'); 203 204 return '<span dir="auto">' . e($place_name) . '</span>'; 205 } 206 207 /** 208 * Generate the place name for display, including the full hierarchy. 209 * 210 * @param bool $link 211 * 212 * @return string 213 */ 214 public function fullName(bool $link = false): string 215 { 216 if ($this->parts->isEmpty()) { 217 return ''; 218 } 219 220 $full_name = $this->parts->implode(I18N::$list_separator); 221 222 if ($link) { 223 return '<a dir="auto" href="' . e($this->url()) . '">' . e($full_name) . '</a>'; 224 } 225 226 return '<span dir="auto">' . e($full_name) . '</span>'; 227 } 228 229 /** 230 * For lists and charts, where the full name won’t fit. 231 * 232 * @param bool $link 233 * 234 * @return string 235 */ 236 public function shortName(bool $link = false): string 237 { 238 $SHOW_PEDIGREE_PLACES = (int) $this->tree->getPreference('SHOW_PEDIGREE_PLACES'); 239 240 // Abbreviate the place name, for lists 241 if ($this->tree->getPreference('SHOW_PEDIGREE_PLACES_SUFFIX')) { 242 $parts = $this->lastParts($SHOW_PEDIGREE_PLACES); 243 } else { 244 $parts = $this->firstParts($SHOW_PEDIGREE_PLACES); 245 } 246 247 $short_name = $parts->implode(I18N::$list_separator); 248 249 // Add a tool-tip showing the full name 250 $title = strip_tags($this->fullName()); 251 252 if ($link) { 253 return '<a dir="auto" href="' . e($this->url()) . '" title="' . $title . '">' . e($short_name) . '</a>'; 254 } 255 256 return '<span dir="auto">' . e($short_name) . '</span>'; 257 } 258} 259