├── resources ├── lang │ ├── en-AU.csv │ ├── en-GB.csv │ ├── en-US.csv │ ├── ca.mo │ ├── cs.mo │ ├── de.mo │ ├── fr.mo │ ├── it.mo │ ├── ms.mo │ ├── nl.mo │ ├── pl.mo │ ├── ru.mo │ ├── sk.mo │ ├── sv.mo │ ├── ta.mo │ ├── tr.mo │ ├── uk.mo │ ├── vi.mo │ └── nb_NO.mo ├── views │ ├── selects │ │ └── location.phtml │ ├── icons │ │ ├── shared-place.phtml │ │ └── record.phtml │ ├── fact-media-shared-place.phtml │ ├── fact-notes-shared-place.phtml │ ├── fact-sources-shared-place.phtml │ ├── edit │ │ ├── icon-fact-create-shared-place.phtml │ │ ├── edit-fact.phtml │ │ ├── new-individual.phtml │ │ ├── link-spouse-to-individual.phtml │ │ └── fact-location-edit.phtml │ ├── modals │ │ ├── create-shared-place.phtml │ │ └── shared-place-fields.phtml │ ├── components │ │ └── select-location.phtml │ ├── shared-place-page-details.phtml │ ├── shared-place-page-menu.phtml │ ├── shared-places-list-page.phtml │ ├── shared-place-page.phtml │ ├── lists │ │ └── locations-table.phtml │ ├── shared-place-page-links.phtml │ ├── data-fix-options.phtml │ └── js │ │ └── webtreesExt.phtml └── css │ ├── webtrees │ └── plac.png │ ├── minimal.css │ └── webtrees.css ├── event.png ├── place.png ├── placeHistory.png ├── createSharedPlace.png ├── WhatsNew ├── WhatsNew2.php ├── WhatsNew0.php ├── WhatsNew3.php └── WhatsNew1.php ├── patchedWebtrees ├── Exceptions │ └── SharedPlaceNotFoundException.php ├── Functions │ └── FunctionsPrintExt.php ├── SharedPlaceParentAt.php ├── Http │ └── RequestHandlers │ │ ├── SharedPlaceRef.php │ │ ├── AutoCompletePlaceExt.php │ │ ├── AbstractTomSelectWithDateHandler.php │ │ ├── CreateSharedPlaceModal.php │ │ ├── TomSelectSharedPlace.php │ │ ├── SharedPlacePage.php │ │ └── CreateSharedPlaceAction.php ├── Elements │ ├── CustomLocationEvent.php │ ├── XrefSharedPlace.php │ ├── LanguageIdReplacement.php │ └── LanguageIdExt.php ├── SharedPlacePreferences.php ├── Services │ ├── GedcomImportServiceExt.php │ └── SearchServiceExt.php ├── Factories │ └── SharedPlaceFactory.php ├── LocGraph.php └── PlaceViaSharedPlace.php ├── po.bat ├── autoload.php ├── module.php ├── HelpTexts.php ├── SharedPlacesListController.php ├── PlaceholderModule.php ├── README.md ├── metadata.json └── SharedPlacesModuleTrait.php /resources/lang/en-AU.csv: -------------------------------------------------------------------------------- 1 | "Location";"Shared place" 2 | "Locations";"Shared places" -------------------------------------------------------------------------------- /resources/lang/en-GB.csv: -------------------------------------------------------------------------------- 1 | "Location";"Shared place" 2 | "Locations";"Shared places" -------------------------------------------------------------------------------- /resources/lang/en-US.csv: -------------------------------------------------------------------------------- 1 | "Location";"Shared place" 2 | "Locations";"Shared places" -------------------------------------------------------------------------------- /resources/views/selects/location.phtml: -------------------------------------------------------------------------------- 1 | primaryPlace()->gedcomName() ?> 2 | -------------------------------------------------------------------------------- /event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/event.png -------------------------------------------------------------------------------- /place.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/place.png -------------------------------------------------------------------------------- /placeHistory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/placeHistory.png -------------------------------------------------------------------------------- /resources/lang/ca.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/ca.mo -------------------------------------------------------------------------------- /resources/lang/cs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/cs.mo -------------------------------------------------------------------------------- /resources/lang/de.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/de.mo -------------------------------------------------------------------------------- /resources/lang/fr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/fr.mo -------------------------------------------------------------------------------- /resources/lang/it.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/it.mo -------------------------------------------------------------------------------- /resources/lang/ms.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/ms.mo -------------------------------------------------------------------------------- /resources/lang/nl.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/nl.mo -------------------------------------------------------------------------------- /resources/lang/pl.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/pl.mo -------------------------------------------------------------------------------- /resources/lang/ru.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/ru.mo -------------------------------------------------------------------------------- /resources/lang/sk.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/sk.mo -------------------------------------------------------------------------------- /resources/lang/sv.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/sv.mo -------------------------------------------------------------------------------- /resources/lang/ta.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/ta.mo -------------------------------------------------------------------------------- /resources/lang/tr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/tr.mo -------------------------------------------------------------------------------- /resources/lang/uk.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/uk.mo -------------------------------------------------------------------------------- /resources/lang/vi.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/vi.mo -------------------------------------------------------------------------------- /createSharedPlace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/createSharedPlace.png -------------------------------------------------------------------------------- /resources/lang/nb_NO.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/lang/nb_NO.mo -------------------------------------------------------------------------------- /resources/css/webtrees/plac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_shared_places/HEAD/resources/css/webtrees/plac.png -------------------------------------------------------------------------------- /resources/views/icons/shared-place.phtml: -------------------------------------------------------------------------------- 1 | 5 | 6 | -------------------------------------------------------------------------------- /resources/css/minimal.css: -------------------------------------------------------------------------------- 1 | .wt-icon-shared-place { 2 | display: inline-block; 3 | vertical-align: middle; 4 | background-repeat: no-repeat; 5 | background-size: cover; 6 | } 7 | 8 | /* properly align non editable place name (hacky) */ 9 | .input-group-replacement { 10 | flex: 100 1 auto; 11 | } -------------------------------------------------------------------------------- /WhatsNew/WhatsNew2.php: -------------------------------------------------------------------------------- 1 | 12 | 13 | gedcom(), $matches, PREG_SET_ORDER) > 0) : ?> 14 |
15 | 16 | $match[1], 'parent' => '_LOC', 'tree' => $fact->tree()]) ?> 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /resources/views/fact-notes-shared-place.phtml: -------------------------------------------------------------------------------- 1 | 12 | 13 | gedcom(), $matches, PREG_SET_ORDER) > 0) : ?> 14 |
15 | 16 | $match[1], 'parent' => '_LOC', 'tree' => $fact->tree()]) ?> 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /resources/views/fact-sources-shared-place.phtml: -------------------------------------------------------------------------------- 1 | 12 | 13 | gedcom(), $matches, PREG_SET_ORDER) > 0) : ?> 14 |
15 | 16 | $match[1], 'parent' => '_LOC', 'tree' => $fact->tree()]) ?> 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /po.bat: -------------------------------------------------------------------------------- 1 | git grep -I --name-only --fixed-strings -e I18N:: -- *.php *.phtml :(exclude)replacedWebtrees/** :(exclude)resources/views/edit/** :(exclude)resources/views/modules/media-list/page_20.phtml | xargs xgettext --package-name=vesta --package-version=1.0 --msgid-bugs-address=ric@richard-cissee.de --output=resources/lang/messages.pot --no-wrap --language=PHP --add-comments=I18N --from-code=utf-8 --keyword --keyword=translate:1 --keyword=translateContext:1c,2 --keyword=plural:1,2 2 | git ls-files *.po | xargs -I INPUT msgmerge --no-wrap --sort-output --no-fuzzy-matching --quiet --backup=off INPUT "resources/lang/messages.pot" -U 3 | pause -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | addPsr4('Cissee\\Webtrees\\Module\\SharedPlaces\\', __DIR__); 7 | $loader->addPsr4('Cissee\\WebtreesExt\\', __DIR__ . "/patchedWebtrees"); 8 | $loader->addPsr4('Cissee\\WebtreesExt\\Services\\', __DIR__ . "/patchedWebtrees/Services"); 9 | $loader->addPsr4('Cissee\\WebtreesExt\\Functions\\', __DIR__ . "/patchedWebtrees/Functions"); 10 | $loader->addPsr4('Cissee\\WebtreesExt\\Exceptions\\', __DIR__ . "/patchedWebtrees/Exceptions"); 11 | $loader->addPsr4('Cissee\\WebtreesExt\\Elements\\', __DIR__ . "/patchedWebtrees/Elements"); 12 | $loader->addPsr4('Cissee\\WebtreesExt\\Http\\RequestHandlers\\', __DIR__ . "/patchedWebtrees/Http/RequestHandlers"); 13 | 14 | $loader->register(); 15 | -------------------------------------------------------------------------------- /resources/views/edit/icon-fact-create-shared-place.phtml: -------------------------------------------------------------------------------- 1 | 7 | 8 | toString(); 11 | ?> 12 | 13 | 27 | -------------------------------------------------------------------------------- /patchedWebtrees/SharedPlaceParentAt.php: -------------------------------------------------------------------------------- 1 | date; 21 | } 22 | 23 | public function getSharedPlace(): ?SharedPlace { 24 | return $this->sharedPlace; 25 | } 26 | 27 | public function getIndexOfFact(): int { 28 | return $this->indexOfFact; 29 | } 30 | 31 | 32 | public function __construct( 33 | GedcomDateInterval $date, 34 | ?SharedPlace $sharedPlace, 35 | int $indexOfFact) { 36 | 37 | $this->date = $date; 38 | $this->sharedPlace = $sharedPlace; 39 | $this->indexOfFact = $indexOfFact; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/SharedPlaceRef.php: -------------------------------------------------------------------------------- 1 | record; 16 | } 17 | 18 | public function existed(): bool { 19 | return $this->existed; 20 | } 21 | 22 | public function created(): int { 23 | return $this->created; 24 | } 25 | 26 | public function parent(): ?SharedPlaceRef { 27 | return $this->parent; 28 | } 29 | 30 | public function __construct( 31 | SharedPlace $record, 32 | bool $existed, 33 | int $created, 34 | ?SharedPlaceRef $parent) { 35 | 36 | $this->record = $record; 37 | $this->existed = $existed; 38 | $this->created = $created; 39 | $this->parent = $parent; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /patchedWebtrees/Elements/CustomLocationEvent.php: -------------------------------------------------------------------------------- 1 | '0:1', 16 | 'DATE' => '0:1', 17 | //'AGE' => '0:1', //should probably be removed from webtrees CustomEvent 18 | 'PLAC' => '0:1', 19 | 'ADDR' => '0:1', 20 | 'EMAIL' => '0:1:?', 21 | 'WWW' => '0:1:?', 22 | 'PHON' => '0:1:?', 23 | 'FAX' => '0:1:?', 24 | 'CAUS' => '0:1', 25 | 'AGNC' => '0:1', 26 | 'RELI' => '0:1', 27 | 'NOTE' => '0:M', 28 | 'OBJE' => '0:M', 29 | 'SOUR' => '0:M', 30 | 'RESN' => '0:1', 31 | ]; 32 | 33 | public function __construct(string $label, $subtags = null) 34 | { 35 | parent::__construct($label, self::VESTA_SUBTAGS); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resources/views/edit/edit-fact.phtml: -------------------------------------------------------------------------------- 1 | $can_edit_raw, 11 | 'fact' => $fact, 12 | 'gedcom' => $gedcom, 13 | 'hidden_url' => $hidden_url, 14 | 'title' => $title, 15 | 'tree' => $tree, 16 | 'url' => $url, 17 | ]); 18 | 19 | //select initializers for modal placeholder ajax-modal-vesta.phtml used via CreateSharedPlaceModal, urgh 20 | $select2Initializers = GovIdEditControlsUtils::accessibleModules($tree, Auth::user()) 21 | ->map(function (GovIdEditControlsInterface $module) { 22 | return $module->govIdEditControlSelectScriptSnippet(); 23 | }) 24 | ->toArray(); 25 | 26 | echo view(VestaUtils::vestaViewsNamespace() . '::modals/ajax-modal-vesta', [ 27 | 'ajax' => false, 28 | 'select2Initializers' => $select2Initializers 29 | ]); 30 | ?> 31 | -------------------------------------------------------------------------------- /resources/views/icons/record.phtml: -------------------------------------------------------------------------------- 1 | 17 | tag() === Individual::RECORD_TYPE) : ?> 18 | 19 | tag() === Family::RECORD_TYPE) : ?> 20 | 21 | tag() === Source::RECORD_TYPE) : ?> 22 | 23 | tag() === Repository::RECORD_TYPE) : ?> 24 | 25 | tag() === Note::RECORD_TYPE) : ?> 26 | 27 | tag() === Media::RECORD_TYPE) : ?> 28 | 29 | tag() === Submitter::RECORD_TYPE) : ?> 30 | 31 | tag() === SharedPlace::RECORD_TYPE) : ?> 32 | 33 | 6 | 7 |
8 | 9 | I18N::translate('Create a shared place')]) ?> 10 | 11 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | getScript(); 30 | } 31 | ?> 32 | 33 | 36 | 39 | -------------------------------------------------------------------------------- /resources/views/components/select-location.phtml: -------------------------------------------------------------------------------- 1 | 22 | 23 | 40 | -------------------------------------------------------------------------------- /patchedWebtrees/SharedPlacePreferences.php: -------------------------------------------------------------------------------- 1 | useHierarchy; 16 | } 17 | 18 | public function useIndirectLinks(): bool { 19 | return $this->useIndirectLinks; 20 | } 21 | 22 | public function addfacts(): array { 23 | return $this->addfacts; 24 | } 25 | 26 | public function uniquefacts(): array { 27 | return $this->uniquefacts; 28 | } 29 | 30 | public function requiredfacts(): array { 31 | return $this->requiredfacts; 32 | } 33 | 34 | public function quickfacts(): array { 35 | return $this->quickfacts; 36 | } 37 | 38 | public function __construct( 39 | bool $useHierarchy, 40 | bool $useIndirectLinks, 41 | array $addfacts, 42 | array $uniquefacts, 43 | array $requiredfacts, 44 | array $quickfacts) { 45 | 46 | $this->useHierarchy = $useHierarchy; 47 | $this->useIndirectLinks = $useIndirectLinks; 48 | $this->addfacts = $addfacts; 49 | $this->uniquefacts = $uniquefacts; 50 | $this->requiredfacts = $requiredfacts; 51 | $this->quickfacts = $quickfacts; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /resources/views/edit/new-individual.phtml: -------------------------------------------------------------------------------- 1 | $facts, 21 | 'gedcom_edit_service' => $gedcom_edit_service, 22 | 'post_url' => $post_url, 23 | 'title' => $title, 24 | 'tree' => $tree, 25 | 'url' => $url, 26 | ]); 27 | 28 | 29 | //select initializers for modal placeholder ajax-modal-vesta.phtml used via CreateSharedPlaceModal, urgh 30 | $select2Initializers = GovIdEditControlsUtils::accessibleModules($tree, Auth::user()) 31 | ->map(function (GovIdEditControlsInterface $module) { 32 | return $module->govIdEditControlSelectScriptSnippet(); 33 | }) 34 | ->toArray(); 35 | 36 | echo view(VestaUtils::vestaViewsNamespace() . '::modals/ajax-modal-vesta', [ 37 | 'ajax' => false, 38 | 'select2Initializers' => $select2Initializers 39 | ]); 40 | ?> 41 | -------------------------------------------------------------------------------- /resources/css/webtrees.css: -------------------------------------------------------------------------------- 1 | .wt-icon-shared-place { 2 | /* plac.png embedded via https://onlineimagetools.com/convert-image-to-data-uri */ 3 | content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wgZCSkm2P+xwQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAADBUlEQVQ4y82TyWucdRjHv7/t3eadfRIzydigQy0VG0QsCq2CBE+Kh6JCL0IRL0FaaMFKQXvwUkqr4sFiPcdE1KJI0aZWQaHRaNrESRPbdyaZyUxmppNttszSd/l5SOv2F/g5PZfvh4dnIfgLCYAAkPTts+tJv1h9MRbMjnQ7FVlvlj68vVq/sJKbtcbHLsp3Tr2Lt948CtxN/AOpjH66cSERmx0ullPazM095WZ9OtB1wohGMoyLyGWfZx84cfKEfS/xt0B4ZipFpmYyzq4fJ5ZLpjIv1jYsXVUkhB72QAda8d7ZuCKMedKHJ44ffL0JAPS98W3ZqfNkJJlY353+7Uq7sOLr/Pr7/qXF4qGUpq2ofiMdjASvxTh3wBU8rNT1EQAYv3juXgeSpBYq7czc1+r0TAr5laIE0wjozqaihrxIpBzoOE9WQaJ2OLwWCIW33COvHvQBBBQAvvrGO07b19Vb6Tyyy2vo3BEERINpZM3N6t4CIY4b8FnB/r5LkVLpkUY6s5edPD32BoBtwaMPWcM3rWUsF8qo1hqglIISAg86emOTifTiIYuzKjHjz7EOe1CvlAc2II1nAYADwOjYbCER30C5vApK6d2lEkAKqFrZP9A/kUhbr2Wb1Ex0m6C20+XpW4EcADAAuPL9A3M3rMZh06jalEsmhArOBYTgYFwlmm4r/YmrwamppyuNllZvuYVY17Fezsx9UWXbQ7y66ZHB3bqqJkOhlkKZCqFwuK7PVhRKDIPgp8lX1rbahqOqWzpc69J3n73w8dnh98EG9x1DLT+JaBQLmZxx4P4BVdMUyrki0Gr7bcOU9PxHj992WEQ2ahmj213QC9YHnzc2M79MLH3rslp+Esld+5BbnF4Nhb0brme8FIuqCmUCfr/kggNNPObu6MuF1zcXjWrlk/FS9vIogPy/LjEx2I9CrgjKEHlq/zNnenp7nhea2fNHdqed3NGqXZ+r/1xc+vJcu577AUCbEAopvf/8AgPgbpdDQ0P3UT0Zr3fCMjt/reLZMyX8L/kTDRpJZHMVLaEAAAAASUVORK5CYII=); 4 | } 5 | 6 | /* properly align non editable place name (hacky) */ 7 | .input-group-replacement { 8 | flex: 100 1 auto; 9 | } 10 | 11 | div.vesta_sp_data { 12 | margin-top: 1em; 13 | } -------------------------------------------------------------------------------- /resources/views/edit/link-spouse-to-individual.phtml: -------------------------------------------------------------------------------- 1 | $cancel_url, 22 | 'facts' => $facts, //undocumented! 23 | 'gedcom_edit_service' => $gedcom_edit_service, 24 | 'label' => $label, 25 | 'post_url' => $post_url, 26 | 'title' => $title, 27 | 'tree' => $tree, 28 | ]); 29 | 30 | //select initializers for modal placeholder ajax-modal-vesta.phtml used via CreateSharedPlaceModal, urgh 31 | $select2Initializers = GovIdEditControlsUtils::accessibleModules($tree, Auth::user()) 32 | ->map(function (GovIdEditControlsInterface $module) { 33 | return $module->govIdEditControlSelectScriptSnippet(); 34 | }) 35 | ->toArray(); 36 | 37 | echo view(VestaUtils::vestaViewsNamespace() . '::modals/ajax-modal-vesta', [ 38 | 'ajax' => false, 39 | 'select2Initializers' => $select2Initializers 40 | ]); 41 | ?> 42 | -------------------------------------------------------------------------------- /resources/views/shared-place-page-details.phtml: -------------------------------------------------------------------------------- 1 | |null $clipboard_facts 12 | * @var GedcomRecord $record 13 | * @var ModuleVestalInterface $module 14 | */ 15 | 16 | ?> 17 | 18 | 19 | 23 | facts([], true) as $fact) : ?> 24 | $fact, 'record' => $record]) 27 | ?> 28 | $fact, 30 | 'record' => $record, 31 | 'module' => $module, 32 | 'useVestals' => false, 33 | 'hideCoordinates' => false, 34 | 'associateFactUtils' => new FallbackAssociateFactUtils(), 35 | 'ownAdditionalStyles' => [], //none 36 | 'predecessors' => [], //not used here 37 | 'relToPredecessorSuffix' => '', //not used here 38 | ]) ?> 39 | 40 |
41 | -------------------------------------------------------------------------------- /patchedWebtrees/Services/GedcomImportServiceExt.php: -------------------------------------------------------------------------------- 1 | make($xref, $tree); 35 | //$record->updatePlaces(); 36 | 37 | //rather mark for check, and handle actual check elsewhere (as before) 38 | // 39 | //Issue #148 40 | //we have to do this in addition to the existing check logic because 41 | //webtrees always resets the placelinks during update 42 | //(even for shared places where our 'inner' cache key doesn't change) 43 | SharedPlace::forget($tree, $xref); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /module.php: -------------------------------------------------------------------------------- 1 | filter(static function (string $filename): bool { 27 | // Special characters will break PHP variable names. 28 | // This also allows us to ignore modules called "foo.example" and "foo.disable" 29 | $module_name = basename(dirname($filename)); 30 | 31 | foreach (['.', ' ', '[', ']'] as $character) { 32 | if (str_contains($module_name, $character)) { 33 | return false; 34 | } 35 | } 36 | 37 | return strlen($module_name) <= 30; 38 | }) 39 | ->each(static function (string $filename): void { 40 | require_once $filename; 41 | }); 42 | 43 | //dependency check 44 | $ok = class_exists("Cissee\WebtreesExt\AbstractModule", true); 45 | if (!$ok) { 46 | FlashMessages::addMessage("Missing dependency - Make sure to install all Vesta modules!"); 47 | return; 48 | } 49 | 50 | $placeholder = \Vesta\VestaUtils::get(PlaceholderModule::class); 51 | return $placeholder->ifIncompatible() ?? \Vesta\VestaUtils::get(SharedPlacesModule::class); 52 | -------------------------------------------------------------------------------- /resources/views/shared-place-page-menu.phtml: -------------------------------------------------------------------------------- 1 | $clipboard_facts 14 | * @var GedcomRecord $record 15 | */ 16 | 17 | ?> 18 | 19 | 53 | -------------------------------------------------------------------------------- /resources/views/modals/shared-place-fields.phtml: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 14 |
15 | 16 | 19 | 20 | 21 | 22 | getMain(); 25 | } 26 | 27 | //TODO integrate $additionalControls better with the following! 28 | //TODO $requiredfacts currently always empty 29 | 30 | //cf new-individual.phtml (must adapt to 2.1) 31 | $tags = new Collection($requiredfacts); 32 | 33 | foreach (Fact::sortFactTags($tags) as $tag) { 34 | if ('NOTE' === $tag) { 35 | echo view('cards/add-note', [ 36 | 'level' => 1, 37 | 'tree' => $tree, 38 | ]); 39 | } else if ('SHARED_NOTE' === $tag) { 40 | echo view('cards/add-shared-note', [ 41 | 'level' => 1, 42 | 'tree' => $tree, 43 | ]); 44 | } //else skip 45 | } 46 | 47 | ?> 48 |
49 | 50 | 51 | 55 | 56 | 57 | 58 | ' . 62 | 'var REF = $(\'' . $selector . '\');' . 63 | '$(\'#shared-place-name\').val(REF.val());' . 64 | ''; 65 | echo $script; 66 | //View::endpush(); 67 | } 68 | ?> 69 | -------------------------------------------------------------------------------- /patchedWebtrees/Elements/XrefSharedPlace.php: -------------------------------------------------------------------------------- 1 | $id, 42 | 'name' => $name, 43 | 'location' => Registry::locationFactory()->make(trim($value, '@'), $tree), 44 | 'tree' => $tree, 45 | 'at' => '@', 46 | ]); 47 | 48 | $selector = '[id$=PLAC]'; 49 | 50 | $route = route(CreateSharedPlaceModal::class, [ 51 | 'tree' => $tree->name(), 52 | 'shared-place-name-selector' => $selector, 53 | ]); 54 | 55 | return 56 | '
' . 57 | '' . 60 | $select . 61 | '
'; 62 | } 63 | 64 | /** 65 | * Display the value of this type of element. 66 | * 67 | * @param string $value 68 | * @param Tree $tree 69 | * 70 | * @return string 71 | */ 72 | public function value(string $value, Tree $tree): string 73 | { 74 | return $this->valueXrefLink($value, $tree, Registry::locationFactory()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /resources/views/shared-places-list-page.phtml: -------------------------------------------------------------------------------- 1 | 8 | 9 |

10 | 11 |

12 | 13 |
14 | 15 |

16 | 17 |

18 |

19 | 20 | 21 |

22 |

23 | 24 |

25 | 26 | 27 | toString(); 30 | ?> 31 | 32 |
33 | 34 | 45 | 46 | 47 |

48 | format() ?> 49 |

50 | 51 |
52 |
53 | 54 | $sharedPlaces, 56 | 'tree' => $tree]) ?> 57 | 58 | false, 64 | 'select2Initializers' => $select2Initializers 65 | ]); 66 | ?> 67 | -------------------------------------------------------------------------------- /HelpTexts.php: -------------------------------------------------------------------------------- 1 | ' . 19 | I18N::translate('The summary shows the shared place data, formatted in the same way as for events with a place mapped to the respective shared place.') . ' ' . 20 | I18N::translate('Therefore, the place name is displayed here including the full hierarchy.') . ' ' . 21 | I18N::translate('You can set a reference year (which may be evaluated by other modules, such as %1$s) in the module configuration.', CommonI18N::titleVestaGov4Webtrees()) . 22 | '

'; 23 | break; 24 | case 'PLAC': 25 | $title = MoreI18N::xlate('Shared place name'); 26 | $text = '

' . 27 | I18N::translate('Place names should be entered as single place name (do not use a comma-separated list here).') . ' ' . 28 | I18N::translate('Use the separate tag \'%1$s\' in order to model a place hierarchy.', GedcomTag::getLabel('_LOC:_LOC')) . 29 | '

' . 30 | '

' . 31 | I18N::translate('Place names can change over time. You can add multiple names to a shared place, and indicate historic names via a suitable date range.') . 32 | '

'; 33 | break; 34 | case 'PLAC_CSV': 35 | $title = MoreI18N::xlate('Shared place name'); 36 | $text = '

' . 37 | I18N::translate('Place names should be entered as a comma-separated list, starting with the smallest place and ending with the country. For example, “Westminster, London, England”.') . 38 | '

' . 39 | '

' . 40 | I18N::translate('Place names can change over time. You can add multiple names to a shared place, and indicate historic names via a suitable date range.') . 41 | '

'; 42 | break; 43 | default: 44 | return HelpTextsPlaceHistory::helpText($help); 45 | } 46 | 47 | return view('modals/help', [ 48 | 'title' => $title, 49 | 'text' => $text, 50 | ]); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/AutoCompletePlaceExt.php: -------------------------------------------------------------------------------- 1 | module_service = $module_service; 33 | $this->search_service_ext = $search_service_ext; 34 | } 35 | 36 | /** 37 | * @param ServerRequestInterface $request 38 | * 39 | * @return Collection 40 | */ 41 | protected function search(ServerRequestInterface $request): Collection 42 | { 43 | $tree = Validator::attributes($request)->tree(); 44 | $query = Validator::queryParams($request)->string('query'); 45 | 46 | $data = $this->search_service_ext 47 | ->searchPlaces($tree, $query, false, 0, static::LIMIT) 48 | ->map(static function (Place $place): string { 49 | return $place->gedcomName(); 50 | }); 51 | 52 | if ($data->count() < static::LIMIT) { 53 | $data = $data->concat($this->search_service_ext 54 | ->searchPlaces($tree, $query, true, 0, static::LIMIT-$data->count()) 55 | ->map(static function (Place $place): string { 56 | return $place->gedcomName(); 57 | })) 58 | //do not sort, keep first results at front, but: 59 | ->unique() //drop duplicates 60 | ->values(); //re-key to 0,1,2,3 ... 61 | } 62 | 63 | // No place found? Use external gazetteers. 64 | foreach ($this->module_service->findByInterface(ModuleMapAutocompleteInterface::class) as $module) { 65 | if ($data->isEmpty()) { 66 | $data = $data->concat($module->searchPlaceNames($query))->sort(); 67 | } 68 | } 69 | 70 | return $data; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/AbstractTomSelectWithDateHandler.php: -------------------------------------------------------------------------------- 1 | tree(); 33 | $at = Validator::queryParams($request)->isInArray(['', '@'])->string('at'); 34 | $page = Validator::queryParams($request)->integer('page', 1); 35 | $query = Validator::queryParams($request)->string('query'); 36 | 37 | $dateStr = Validator::queryParams($request)->string('dateStr', ''); 38 | $date = ($dateStr === '')?GedcomDateInterval::createNow():GedcomDateInterval::create($dateStr); 39 | 40 | // Fetch one more row than we need, so we can know if more rows exist. 41 | $offset = ($page - 1) * self::RESULTS_PER_PAGE; 42 | $limit = self::RESULTS_PER_PAGE + 1; 43 | 44 | // Perform the search. 45 | if ($query !== '') { 46 | $results = $this->search($tree, $date, $query, $offset, $limit, $at ? '@' : ''); 47 | } else { 48 | $results = new Collection(); 49 | } 50 | 51 | if ($results->count() > self::RESULTS_PER_PAGE) { 52 | $next_url = route(static::class, ['tree' => $tree->name(), 'at' => $at ? '@' : '', 'page' => $page + 1]); 53 | } else { 54 | $next_url = null; 55 | } 56 | 57 | return response([ 58 | 'data' => $results->slice(0, self::RESULTS_PER_PAGE)->all(), 59 | 'nextUrl' => $next_url, 60 | ]); 61 | } 62 | 63 | /** 64 | * Perform the search 65 | * 66 | * @param Tree $tree 67 | * @param string $query 68 | * @param int $offset 69 | * @param int $limit 70 | * @param string $at "@" or "" 71 | * 72 | * @return Collection 73 | */ 74 | abstract protected function search(Tree $tree, GedcomDateInterval $date, string $query, int $offset, int $limit, string $at): Collection; 75 | } 76 | -------------------------------------------------------------------------------- /patchedWebtrees/Elements/LanguageIdReplacement.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | public function values(): array 37 | { 38 | $locales = LanguageIdExt::values(); 39 | 40 | $coll = new Collection($locales); 41 | 42 | $values = $coll 43 | ->mapWithKeys(static function (LocaleInterface $locale, string $key): array { 44 | if ($key === $locale->endonym()) { 45 | return [strtoupper($key) => $key]; 46 | } 47 | return [strtoupper($key) => $key . ' ('. $locale->endonym() . ')']; 48 | }) 49 | ->all(); 50 | 51 | uasort($values, I18N::comparator()); 52 | 53 | return $values; 54 | } 55 | 56 | public function valuesG7(): array 57 | { 58 | $locales = LanguageIdExt::values(); 59 | 60 | $coll = new Collection($locales); 61 | 62 | $values = $coll 63 | ->mapWithKeys(static function (LocaleInterface $locale, string $key): array { 64 | $keyViaCode = $locale->language()->code(); 65 | if ($key === $locale->endonym()) { 66 | return [strtoupper($keyViaCode) => $key]; 67 | } 68 | return [strtoupper($keyViaCode) => $key . ' ('. $locale->endonym() . ')']; 69 | }) 70 | ->all(); 71 | 72 | uasort($values, I18N::comparator()); 73 | 74 | return $values; 75 | } 76 | 77 | /** 78 | * Display the value of this type of element. 79 | * 80 | * @param string $value 81 | * @param Tree $tree 82 | * 83 | * @return string 84 | */ 85 | public function value(string $value, Tree $tree): string 86 | { 87 | $values = $this->values(); 88 | 89 | $canonical = $this->canonical($value); 90 | 91 | //issue #149: also properly display gedcom 7 language tags (no edit support yet) 92 | $valuesG7 = $this->valuesG7(); 93 | 94 | return $values[$canonical] ?? $valuesG7[$canonical] ?? '' . e($value) . ''; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SharedPlacesListController.php: -------------------------------------------------------------------------------- 1 | module = $module; 34 | $this->moduleName = $module->name(); 35 | $this->hasLocationsToFix = $hasLocationsToFix; 36 | $this->link = $link; 37 | } 38 | 39 | public function sharedPlacesList(Tree $tree, $showLinkCounts): ResponseInterface { 40 | $sharedPlaces = SharedPlacesListController::allSharedPlaces($tree); 41 | 42 | //select initializers for modal placeholder ajax-modal-vesta.phtml used via CreateSharedPlaceModal, urgh 43 | $select2Initializers = GovIdEditControlsUtils::accessibleModules($tree, Auth::user()) 44 | ->map(function (GovIdEditControlsInterface $module) { 45 | return $module->govIdEditControlSelectScriptSnippet(); 46 | }) 47 | ->toArray(); 48 | 49 | return $this->viewResponse($this->moduleName . '::shared-places-list-page', [ 50 | 'tree' => $tree, 51 | 'sharedPlaces' => $sharedPlaces, 52 | 'showLinkCounts' => $showLinkCounts, 53 | 'title' => I18N::translate('Shared places'), 54 | 'moduleName' => $this->moduleName, 55 | 'select2Initializers' => $select2Initializers, 56 | 'hasLocationsToFix' => $this->hasLocationsToFix, 57 | 'link' => $this->link, 58 | ]); 59 | } 60 | 61 | /** 62 | * Find all the shared place records in a tree. 63 | * 64 | * @param Tree $tree 65 | * 66 | * @return Collection 67 | */ 68 | private function allSharedPlaces(Tree $tree): Collection { 69 | /* 70 | $count = DB::table('other') 71 | ->where('o_file', '=', $tree->id()) 72 | ->where('o_type', '=', '_LOC') 73 | ->count(); 74 | 75 | error_log("count".$count); 76 | */ 77 | 78 | return DB::table('other') 79 | ->where('o_file', '=', $tree->id()) 80 | ->where('o_type', '=', '_LOC') 81 | ->get() 82 | ->map(Registry::locationFactory()->mapper($tree)) 83 | ->filter(GedcomRecord::accessFilter()); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /resources/views/shared-place-page.phtml: -------------------------------------------------------------------------------- 1 | |null $clipboard_facts 12 | * @var Collection|null $linked_families 13 | * @var Collection|null $linked_individuals 14 | * 15 | * //[RC] added 16 | * @var Collection|null $llSharedPlaces 17 | * 18 | * @var Collection|null $linked_media_objects 19 | * @var Collection|null $linked_notes 20 | * @var Collection|null $linked_sources 21 | * @var GedcomRecord $record 22 | * @var Tree $tree 23 | * 24 | * @var string $hierarchyHtml 25 | * @var string $summaryHtml 26 | * 27 | * @var ModuleVestalInterface $module 28 | * 29 | * //$module must also provide getHelpAction for 'Predecessor'! 30 | * //$module may also provide preferences for 'RESTRICTED_PLACE_HISTORY'! 31 | */ 32 | 33 | ?> 34 | 35 | $record]) ?> 36 | 37 | 38 |
39 |

40 | fullName() ?> 41 |

42 | canEdit()) : ?> 43 | $clipboard_facts, 'record' => $record]) ?> 44 | 45 |
46 | 47 |
48 | 49 | view($moduleName . '::shared-place-page-details', [ 51 | 'hierarchyHtml' => $hierarchyHtml, 52 | 'summaryHtml' => $summaryHtml, 53 | 'clipboard_facts' => $clipboard_facts, 54 | 'record' => $record, 55 | 'module' => $module, 56 | ]), 57 | 58 | 'linked_families' => $linked_families, 59 | 'linked_individuals' => $linked_individuals, 60 | 'llSharedPlaces' => $llSharedPlaces, 61 | 'linked_media_objects' => $linked_media_objects, 62 | 'linked_notes' => $linked_notes, 63 | 'linked_sources' => $linked_sources, 64 | 'tree' => $tree, 65 | 66 | 'record' => $record, 67 | 'module' => $module, 68 | ]) ?> 69 | 70 | $hierarchyHtml, 72 | 'summaryHtml' => $summaryHtml, 73 | 'clipboard_facts' => $clipboard_facts, 74 | 'record' => $record, 75 | 'module' => $module, 76 | ]) ?> 77 | 78 |
79 | 80 | 83 | 84 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/CreateSharedPlaceModal.php: -------------------------------------------------------------------------------- 1 | module = $module; 29 | $this->moduleName = $module->name(); 30 | } 31 | 32 | /** 33 | * Show a form to create a new shared place object. 34 | * 35 | * @param ServerRequestInterface $request 36 | * 37 | * @return ResponseInterface 38 | */ 39 | public function handle(ServerRequestInterface $request): ResponseInterface 40 | { 41 | $tree = Validator::attributes($request)->tree(); 42 | $sharedPlaceName = Validator::queryParams($request)->string('shared-place-name', ''); 43 | $selector = Validator::queryParams($request)->string('shared-place-name-selector', ''); 44 | 45 | //requires modal placeholder in SharedPlacesListController.sharedPlacesList(), uargh 46 | //also requires modal placeholder in edit fact! meh. 47 | //also requires modal placeholder in SharedPlacesModule.hFactsTabGetAdditionalEditControls(), 48 | //handled via hFactsTabRequiresModalVesta! 49 | 50 | $additionalControls = GovIdEditControlsUtils::accessibleModules($tree, Auth::user()) 51 | ->map(function (GovIdEditControlsInterface $module) use ($sharedPlaceName) { 52 | return $module->govIdEditControl( 53 | null, 54 | 'shared-place-govId', 55 | 'shared-place-govId', 56 | $sharedPlaceName, 57 | '#shared-place-name', //cf shared-place-fields.phtml 58 | true, 59 | true); 60 | }) 61 | ->toArray(); 62 | 63 | $useHierarchy = boolval($this->module->getPreference('USE_HIERARCHY', '1')); 64 | 65 | $requiredfactsStr = $this->module->getPreference('_LOC_FACTS_REQUIRED', ''); 66 | $requiredfacts = FunctionsPrintExt::adjust(preg_split("/[, ]+/", $requiredfactsStr, -1, PREG_SPLIT_NO_EMPTY)); 67 | 68 | return response(view($this->moduleName . '::modals/create-shared-place', [ 69 | 'moduleName' => $this->moduleName, 70 | 'useHierarchy' => $useHierarchy, 71 | 'sharedPlaceName' => $sharedPlaceName, 72 | 'selector' => $selector, 73 | 'additionalControls' => $additionalControls, 74 | 'requiredfacts' => $requiredfacts, 75 | 'tree' => $tree, 76 | ])); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /PlaceholderModule.php: -------------------------------------------------------------------------------- 1 | minRequiredWebtreesVersion(); 52 | 53 | //min version check 54 | $version_ok = version_compare(Webtrees::VERSION, $min_version) >= 0; 55 | if (!$version_ok) { 56 | return CommonI18N::noopModuleMin($min_version); 57 | } 58 | 59 | $max_version = $this->minUnsupportedWebtreesVersion(); 60 | 61 | //max version check (allow current dev version though) 62 | $version_ok = (Webtrees::VERSION === $max_version.'-dev') || (version_compare($max_version, Webtrees::VERSION) > 0); 63 | if (!$version_ok) { 64 | return CommonI18N::noopModuleMax($max_version); 65 | } 66 | 67 | return ''; 68 | } 69 | 70 | public function ifIncompatible(): ?PlaceholderModule { 71 | $min_version = $this->minRequiredWebtreesVersion(); 72 | 73 | //min version check 74 | $version_ok = version_compare(Webtrees::VERSION, $min_version) >= 0; 75 | if (!$version_ok) { 76 | return $this; 77 | } 78 | 79 | $max_version = $this->minUnsupportedWebtreesVersion(); 80 | 81 | //max version check (allow current dev version though) 82 | $version_ok = (Webtrees::VERSION === $max_version.'-dev') || (version_compare($max_version, Webtrees::VERSION) > 0); 83 | if (!$version_ok) { 84 | return $this; 85 | } 86 | 87 | return null; 88 | } 89 | 90 | public function boot(): void { 91 | //flash, but only once per day 92 | $title = $this->title(); 93 | 94 | $cache = Registry::cache()->file(); 95 | $key = $title . '-placeholder-flash'; 96 | $cache->remember($key, function () use ($title) { 97 | FlashMessages::addMessage(CommonI18N::noopModuleMessage($title)); 98 | }, 24*3600); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /patchedWebtrees/Factories/SharedPlaceFactory.php: -------------------------------------------------------------------------------- 1 | preferences = $preferences; 26 | } 27 | 28 | public function unmake(string $xref, Tree $tree) { 29 | Registry::cache()->array()->forget(__CLASS__ . $xref . '@' . $tree->id()); 30 | } 31 | 32 | /** 33 | * Create a shared place. 34 | * 35 | * @param string $xref 36 | * @param Tree $tree 37 | * @param string|null $gedcom 38 | * 39 | * @return Location|null 40 | */ 41 | public function make(string $xref, Tree $tree, string $gedcom = null): ?Location { 42 | $ret = Registry::cache()->array()->remember(__CLASS__ . $xref . '@' . $tree->id(), function () use ($xref, $tree, $gedcom) { 43 | $gedcom = $gedcom ?? $this->gedcom($xref, $tree); 44 | $pending = $this->pendingChanges($tree)->get($xref); 45 | 46 | if ($gedcom === null && ($pending === null || !preg_match(self::TYPE_CHECK_REGEX, $pending))) { 47 | return null; 48 | } 49 | 50 | $xref = $this->extractXref($gedcom ?? $pending, $xref); 51 | 52 | return new SharedPlace($this->preferences, $xref, $gedcom ?? '', $pending, $tree); 53 | }); 54 | 55 | if ($ret === null) { 56 | return $ret; 57 | } 58 | 59 | //check 60 | $cacheKey = __CLASS__ . $xref . '@' . $tree->id() . '#CHECK'; 61 | Registry::cache()->array()->remember($cacheKey, function () use ($cacheKey, $ret) { 62 | //not cached: add 'dummy' cache entry first so that we won't loop endlessly in case of circularity 63 | Registry::cache()->array()->remember($cacheKey, function () { 64 | return true; 65 | }); 66 | 67 | //now actually check 68 | $ret->check(); 69 | 70 | return true; 71 | }); 72 | return $ret; 73 | } 74 | 75 | /** 76 | * Create a SharedPlace object from a row in the database. 77 | * 78 | * @param Tree $tree 79 | * 80 | * @return Closure 81 | */ 82 | public function mapper(Tree $tree): Closure { 83 | return function (stdClass $row) use ($tree): SharedPlace { 84 | $sharedPlace = $this->make($row->o_id, $tree, $row->o_gedcom); 85 | assert($sharedPlace instanceof SharedPlace); 86 | return $sharedPlace; 87 | }; 88 | } 89 | 90 | /** 91 | * Create a SharedPlace object from raw GEDCOM data. 92 | * 93 | * @param string $xref 94 | * @param string $gedcom an empty string for new/pending records 95 | * @param string|null $pending null for a record with no pending edits, 96 | * empty string for records with pending deletions 97 | * @param Tree $tree 98 | * 99 | * @return SharedPlace 100 | */ 101 | public function new(string $xref, string $gedcom, ?string $pending, Tree $tree): Location { 102 | return new SharedPlace($this->preferences, $xref, $gedcom, $pending, $tree); 103 | } 104 | 105 | /** 106 | * Fetch GEDCOM data from the database. 107 | * 108 | * @param string $xref 109 | * @param Tree $tree 110 | * 111 | * @return string|null 112 | */ 113 | public function gedcom(string $xref, Tree $tree): ?string { 114 | return DB::table('other') 115 | ->where('o_id', '=', $xref) 116 | ->where('o_file', '=', $tree->id()) 117 | ->whereIn('o_type', [ 118 | SharedPlace::RECORD_TYPE 119 | ]) 120 | ->value('o_gedcom'); 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/TomSelectSharedPlace.php: -------------------------------------------------------------------------------- 1 | search_service = $search_service; 36 | } 37 | 38 | /** 39 | * Perform the search 40 | * 41 | * @param Tree $tree 42 | * @param GedcomDateInterval $date 43 | * @param string $query 44 | * @param int $offset 45 | * @param int $limit 46 | * @param string $at 47 | * 48 | * @return Collection 49 | */ 50 | protected function search( 51 | Tree $tree, 52 | GedcomDateInterval $date, 53 | string $query, 54 | int $offset, 55 | int $limit, 56 | string $at): Collection { 57 | 58 | error_log("!!!"); 59 | 60 | // Search by XREF 61 | $location = Registry::locationFactory()->make($query, $tree); 62 | 63 | $paginate = false; 64 | 65 | if ($location instanceof Location) { 66 | $results = new Collection([$location]); 67 | } else { 68 | //experimental: always go via places only (to avoid matching on other parts of shared place gedcom) 69 | 70 | //#172: add parameter startsWith ("true" := "does NOT have to start with") 71 | $places = $this->search_service->searchPlaces($tree, $query, true); 72 | 73 | $results = $this->search_service->searchLocationsInPlaces($tree, $places); 74 | $paginate = true; 75 | 76 | /* 77 | if (str_contains($query,',')) { 78 | //[PATCHED] 79 | //extended in order to find hierarchical shared places 80 | //overall not very efficient 81 | //TODO strictly only required if hierarchical shared places are enabled! 82 | 83 | $places = $this->search_service->searchPlaces($tree, $query); 84 | 85 | $results1 = $this->search_service->searchLocationsInPlaces($tree, $places); 86 | 87 | //add 'regular' results 88 | //TODO: remove matches on other parts of shared place gedcom? 89 | $results2 = $this->search_service->searchLocations([$tree], [$query]); 90 | 91 | $results = $results1->merge($results2) 92 | 93 | //skip duplicates 94 | ->unique(); 95 | 96 | $paginate = true; 97 | 98 | } else { 99 | $search = array_filter(explode(' ', $query)); 100 | //TODO: remove matches on other parts of shared place gedcom? 101 | $results = $this->search_service->searchLocations([$tree], $search, $offset, $limit); 102 | } 103 | */ 104 | } 105 | 106 | //[PATCHED] 107 | $ret = $results 108 | 109 | ->map(static function (Location $location) use ($at, $date, $query): array { 110 | return [ 111 | 'value' => $at . $location->xref() . $at, 112 | 'text' => view('selects/location', ['location' => $location]), 113 | 'title' => $location->primaryPlaceAt($date, $query)->gedcomName(), 114 | ]; 115 | }) 116 | 117 | //sort 118 | ->sort(static function (array $x, array $y): int { 119 | return $x['text'] <=> $y['text']; 120 | }); 121 | 122 | if ($paginate) { 123 | $ret = $ret->slice($offset, $limit+$offset); 124 | } 125 | 126 | //re-key for https://github.com/laravel/framework/issues/1335 127 | return $ret->values(); 128 | 129 | /* 130 | // Search by XREF 131 | $location = Registry::locationFactory()->make($query, $tree); 132 | 133 | if ($location instanceof Location) { 134 | $results = new Collection([$location]); 135 | } else { 136 | $search = array_filter(explode(' ', $query)); 137 | $results = $this->search_service->searchLocations([$tree], $search, $offset, $limit); 138 | } 139 | 140 | return $results->map(static function (Location $location) use ($at): array { 141 | return [ 142 | 'text' => view('selects/location', ['location' => $location]), 143 | 'value' => $at . $location->xref() . $at, 144 | ]; 145 | }); 146 | */ 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /resources/views/lists/locations-table.phtml: -------------------------------------------------------------------------------- 1 | getPreference('LINK_COUNTS', '0')); 12 | 13 | if ($showLinkCounts) { 14 | //note: performance isn't great in case of $useIndirectLinks - don't see a way to improve this 15 | //thus $showLinkCounts 16 | 17 | foreach ($locations as $sharedPlace) { 18 | $count_individuals[$sharedPlace->xref()] = $sharedPlace->countLinkedIndividuals('_LOC'); 19 | $count_families[$sharedPlace->xref()] = $sharedPlace->countLinkedFamilies('_LOC'); 20 | } 21 | } 22 | 23 | ?> 24 | 25 |
26 | 29 | data-columns=" true], 33 | ['visible' => true], 34 | ['visible' => true], 35 | ['visible' => true], 36 | ['visible' => true], 37 | ['visible' => $showLinkCounts], 38 | ['visible' => $showLinkCounts] 39 | ])) 40 | ?>" 41 | > 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 83 | 84 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 101 | 104 | 105 | 106 | 111 | 112 | 113 | 116 | 117 | 118 | 121 | 122 | 123 | 124 |
43 | 44 |
64 | namesNN()[$sharedPlace->getPrimaryName()]; 66 | ?> 67 | 68 | 69 | 70 |
71 | namesNN() as $name) : ?> 72 | 77 | 78 | 79 | 80 |
81 | 82 |
"> 85 | getAttributes("TYPE") as $type) : ?> 86 | 87 |
88 | 89 |
94 | primaryPlace()->shortName(true) ?> 95 | 99 | printLati() ?> 100 | 102 | printLong() ?> 103 | 107 | facts(['_GOV'])->isNotEmpty()) : ?> 108 | 109 | 110 | 114 | xref()] ?? 0) ?> 115 | 119 | xref()] ?? 0) ?> 120 |
125 |
126 | -------------------------------------------------------------------------------- /resources/views/edit/fact-location-edit.phtml: -------------------------------------------------------------------------------- 1 | > $map_bounds 12 | * @var $marker_position 13 | * @var object $leaflet_config 14 | */ 15 | 16 | //[RC] adjusted 17 | $name = 'TODO'; //$location->locationName(); 18 | 19 | ?> 20 | 21 |
22 |
23 |
    24 |
    25 | 26 | 27 | 33 | 34 | 35 | 36 | 122 | 123 | 124 | 125 | 152 | 153 | -------------------------------------------------------------------------------- /patchedWebtrees/LocGraph.php: -------------------------------------------------------------------------------- 1 | indi = $indi; 25 | $this->fam = $fam; 26 | $this->sour = $sour; 27 | $this->loc = $loc; 28 | } 29 | 30 | public function linkedIndividuals(Collection $locXrefs): Collection { 31 | 32 | $ret = new Collection(); 33 | $handled = new Collection(); 34 | 35 | //safer wrt loops (than to use method recursively) 36 | $queue = new Collection(); 37 | foreach ($locXrefs as $locXref) { 38 | $queue->prepend($locXref); 39 | } 40 | 41 | while ($queue->count() > 0) { 42 | $current = $queue->pop(); 43 | 44 | $handled->add($current); 45 | if (array_key_exists($current, $this->indi)) { 46 | foreach ($this->indi[$current] as $indi => $row) { 47 | $ret->add($row); 48 | } 49 | } 50 | 51 | if (array_key_exists($current, $this->loc)) { 52 | foreach ($this->loc[$current] as $next => $row) { 53 | if (!$handled->contains($next)) { 54 | $queue->prepend($next); 55 | } 56 | } 57 | } 58 | } 59 | 60 | return $ret->unique(); 61 | } 62 | 63 | public function linkedFamilies(Collection $locXrefs): Collection { 64 | $ret = new Collection(); 65 | $handled = new Collection(); 66 | 67 | //safer wrt loops (than to use method recursively) 68 | $queue = new Collection(); 69 | foreach ($locXrefs as $locXref) { 70 | $queue->prepend($locXref); 71 | } 72 | 73 | while ($queue->count() > 0) { 74 | $current = $queue->pop(); 75 | 76 | $handled->add($current); 77 | if (array_key_exists($current, $this->fam)) { 78 | foreach ($this->fam[$current] as $fam => $row) { 79 | $ret->add($row); 80 | } 81 | } 82 | 83 | if (array_key_exists($current, $this->loc)) { 84 | foreach ($this->loc[$current] as $next => $row) { 85 | if (!$handled->contains($next)) { 86 | $queue->prepend($next); 87 | } 88 | } 89 | } 90 | } 91 | 92 | return $ret->unique(); 93 | } 94 | 95 | public function linkedSources(Collection $locXrefs): Collection { 96 | $ret = new Collection(); 97 | $handled = new Collection(); 98 | 99 | //safer wrt loops (than to use method recursively) 100 | $queue = new Collection(); 101 | foreach ($locXrefs as $locXref) { 102 | $queue->prepend($locXref); 103 | } 104 | 105 | while ($queue->count() > 0) { 106 | $current = $queue->pop(); 107 | 108 | $handled->add($current); 109 | if (array_key_exists($current, $this->sour)) { 110 | foreach ($this->sour[$current] as $sour => $row) { 111 | $ret->add($row); 112 | } 113 | } 114 | 115 | if (array_key_exists($current, $this->loc)) { 116 | foreach ($this->loc[$current] as $next => $row) { 117 | if (!$handled->contains($next)) { 118 | $queue->prepend($next); 119 | } 120 | } 121 | } 122 | } 123 | 124 | return $ret->unique(); 125 | } 126 | 127 | public static function get(Tree $tree): LocGraph { 128 | return Registry::cache()->array()->remember('locGraph', function () use ($tree) { 129 | return LocGraph::create($tree); 130 | }); 131 | } 132 | 133 | protected static function create(Tree $tree): LocGraph { 134 | $indi = array(); 135 | $fam = array(); 136 | $sour = array(); 137 | $loc = array(); 138 | 139 | $query = DB::table('link') 140 | ->leftJoin('individuals', function (JoinClause $join): void { 141 | $join 142 | ->on('link.l_from', '=', 'individuals.i_id') 143 | ->on('link.l_file', '=', 'individuals.i_file'); 144 | }) 145 | ->leftJoin('families', function (JoinClause $join): void { 146 | $join 147 | ->on('link.l_from', '=', 'families.f_id') 148 | ->on('link.l_file', '=', 'families.f_file'); 149 | }) 150 | ->leftJoin('sources', function (JoinClause $join): void { 151 | $join 152 | ->on('link.l_from', '=', 'sources.s_id') 153 | ->on('link.l_file', '=', 'sources.s_file'); 154 | }) 155 | ->leftJoin('other', function (JoinClause $join): void { 156 | $join 157 | ->on('link.l_from', '=', 'other.o_id') 158 | ->on('link.l_file', '=', 'other.o_file') 159 | ->where('other.o_type', '=', "_LOC"); 160 | }) 161 | ->where('l_file', '=', $tree->id()) 162 | ->where('l_type', '=', '_LOC') 163 | ->select(['l_from', 'l_to', 'i_id', 'i_gedcom', 'f_id', 'f_gedcom', 's_id', 's_gedcom', 'o_id']); 164 | 165 | $rows = $query->get(); 166 | 167 | foreach ($rows as $row) { 168 | if ($row->i_id !== null) { 169 | $indi[$row->l_to][$row->l_from] = $row; 170 | } else if ($row->f_id !== null) { 171 | $fam[$row->l_to][$row->l_from] = $row; 172 | } else if ($row->s_id !== null) { 173 | $sour[$row->l_to][$row->l_from] = $row; 174 | } else if ($row->o_id !== null) { 175 | $loc[$row->l_to][$row->l_from] = $row; 176 | } //else some other type which we currently don't care about 177 | } 178 | 179 | return new LocGraph($indi, $fam, $sour, $loc); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /resources/views/shared-place-page-links.phtml: -------------------------------------------------------------------------------- 1 | $linked_families 21 | * @var ?Collection $linked_individuals 22 | * 23 | * //[RC] added 24 | * @var ?Collection $llSharedPlaces 25 | * 26 | * @var ?Collection $linked_media_objects 27 | * @var ?Collection $linked_notes 28 | * @var ?Collection $linked_sources 29 | * @var Tree $tree 30 | * 31 | * @var GedcomRecord $record 32 | * @var ModuleVestalInterface $module 33 | * 34 | * //$module must also provide getHelpAction for 'Predecessor'! 35 | * //$module may also provide preferences for 'RESTRICTED_PLACE_HISTORY'! 36 | */ 37 | 38 | $factFilter = new class($module, $record) implements FactFilter { 39 | 40 | //gah painful 41 | private $module; 42 | private $record; 43 | 44 | public function __construct($module, $record) { 45 | $this->module = $module; 46 | $this->record = $record; 47 | } 48 | 49 | //performance doesn't look great 50 | public function filter(Fact $fact): bool { 51 | $ps = PlaceStructure::fromFact($fact); 52 | if ($ps === null) { 53 | return false; 54 | } 55 | $loc = FunctionsPlaceUtils::plac2loc($this->module, $ps, false); 56 | if ($loc === null) { 57 | return false; 58 | } 59 | return ($loc->getXref() === $this->record->xref()); 60 | } 61 | }; 62 | 63 | ?> 64 | 65 | 132 | 133 |
    134 |
    135 | 136 |
    137 | 138 | 139 |
    140 | $linked_individuals, 'sosa' => false, 'tree' => $tree]) ?> 141 |
    142 | 143 | 144 | 145 |
    146 | $linked_families, 'tree' => $tree]) ?> 147 |
    148 | 149 | 150 | 151 |
    152 | $llSharedPlaces, 'tree' => $tree]) ?> 153 |
    154 | 155 | 156 | 157 |
    158 | $linked_media_objects, 'tree' => $tree]) ?> 159 |
    160 | 161 | 162 | 163 |
    164 | $linked_sources, 'tree' => $tree]) ?> 165 |
    166 | 167 | 168 | 169 |
    170 | $linked_notes, 'tree' => $tree]) ?> 171 |
    172 | 173 | 174 |
    175 | $linked_individuals, 177 | 'record' => $record, 178 | 'factFilter' => $factFilter, 179 | 'tree' => $tree, 180 | 'context' => 'sp', 181 | 'module' => $module]) ?> 182 |
    183 |
    184 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/SharedPlacePage.php: -------------------------------------------------------------------------------- 1 | 'NAME', 39 | 2 => 'TYPE', 40 | 3 => '_LOC', 41 | 4 => 'MAP', 42 | 5 => '_GOV', 43 | 'ABBR', 44 | 'AUTH', 45 | 'DATA', 46 | 'PUBL', 47 | 'TEXT', 48 | 'REPO', 49 | 'NOTE', 50 | 'OBJE', 51 | 'REFN', 52 | 'RIN', 53 | '_UID', 54 | 'CHAN', 55 | 'RESN', 56 | ]; 57 | 58 | protected $module; 59 | protected LinkedRecordService $linked_record_service; 60 | protected ClipboardService $clipboard_service; 61 | 62 | public function __construct( 63 | ModuleService $moduleService, 64 | LinkedRecordService $linked_record_service, 65 | ClipboardService $clipboard_service) { 66 | 67 | //access level irrelevant here: there is no way to configure an access level for this specific functionality 68 | //(it's not a list, chart, etc. - we'd have to define it specifically) 69 | $this->module = $moduleService->findByInterface(SharedPlacesModule::class, false)->first(); 70 | 71 | //otherwise we wouldn't even get here (router redirects) 72 | assert ($this->module instanceof SharedPlacesModule); 73 | 74 | $this->linked_record_service = $linked_record_service; 75 | $this->clipboard_service = $clipboard_service; 76 | } 77 | 78 | /** 79 | * @param ServerRequestInterface $request 80 | * 81 | * @return ResponseInterface 82 | */ 83 | public function handle(ServerRequestInterface $request): ResponseInterface { 84 | 85 | $tree = Validator::attributes($request)->tree(); 86 | $xref = Validator::attributes($request)->isXref()->string('xref'); 87 | $slug = Validator::attributes($request)->string('slug', ''); 88 | $record = Registry::locationFactory()->make($xref, $tree); 89 | 90 | //we don't need a specific method here, 91 | //if we ever refactor this: 92 | //use SharedPlaceNotFoundException! 93 | $record = Auth::checkLocationAccess($record, false); 94 | 95 | // Redirect to correct xref/slug 96 | if ($record->xref() !== $xref || Registry::slugFactory()->make($record) !== $slug) { 97 | return redirect($record->url(), StatusCodeInterface::STATUS_MOVED_PERMANENTLY); 98 | } 99 | 100 | $canonical = $record->primaryPlace()->gedcomName(); 101 | 102 | $hierarchyHtml = ''; 103 | 104 | //what was the point of this? summary shows same data! 105 | /* 106 | $mainForDisplay = $sharedPlace->fullName(); 107 | $canonicalForDisplay = $sharedPlace->primaryPlace()->fullName(); 108 | 109 | if ($mainForDisplay !== $canonicalForDisplay) { 110 | $hierarchyHtml = '' . I18N::translate('Shared place hierarchy') . '' . $canonicalForDisplay . ''; 111 | } 112 | */ 113 | 114 | //summary (with any additional data such as GOV data, map links etc), 115 | //if there is a module that provides this summary 116 | $summaryHtml = ''; 117 | if (!empty($record->namesNN())) { 118 | $refYear = intVal($this->module->getPreference('REF_YEAR', '')); 119 | if ($refYear) { 120 | $ps = PlaceStructure::fromNameAndLocWithYear($refYear, $canonical, $record->xref(), $record->tree(), 0, $record); 121 | } else { 122 | $ps = PlaceStructure::fromNameAndLocNow($canonical, $record->xref(), $record->tree(), 0, $record); 123 | } 124 | 125 | if ($ps !== null) { 126 | $summaryGve = FunctionsPlaceUtils::plac2html($this->module, $ps); 127 | 128 | //if ($summaryHtml !== '') { 129 | $summaryHtml = '' . I18N::translate('Summary') . FunctionsPrintExtHelpLink::helpLink($this->module->name(), 'Summary') . '' . $summaryGve->getMain() . ''; 130 | //TODO: handle getScript()! 131 | //} 132 | } 133 | } 134 | 135 | return $this->viewResponse($this->module->name() . '::shared-place-page', [ 136 | 'module' => $this->module, 137 | 'moduleName' => $this->module->name(), 138 | 'hierarchyHtml' => $hierarchyHtml, 139 | 'summaryHtml' => $summaryHtml, 140 | 'facts' => $this->facts($record), 141 | 142 | 'clipboard_facts' => $this->clipboard_service->pastableFacts($record), 143 | //no, must use own impl in case of indirect links 144 | //'linked_individuals' => $this->linked_record_service->linkedIndividuals($record, '_LOC'), 145 | //'linked_families' => $this->linked_record_service->linkedFamilies($record, '_LOC'), 146 | 'linked_individuals' => $record->linkedIndividuals('_LOC'), 147 | 'linked_families' => $record->linkedFamilies('_LOC'), 148 | 'llSharedPlaces' => $this->linked_record_service->linkedLocations($record, '_LOC'), 149 | 'linked_media_objects' => null, //TODO 150 | 'linked_notes' => null, //TODO 151 | 'linked_sources' => $record->linkedSources('_LOC'), 152 | 'meta_robots' => 'index,follow', 153 | 'record' => $record, 154 | 'title' => $record->fullName(), 155 | 'tree' => $tree, 156 | ]); 157 | } 158 | 159 | private function facts(SharedPlace $record): Collection { 160 | 161 | $factsArray = $record->facts()->toArray(); 162 | 163 | $facts = $record->facts() 164 | ->sort(function (Fact $x, Fact $y) use ($factsArray): int { 165 | [, $tag_x] = explode(':', $x->tag()); 166 | [, $tag_y] = explode(':', $y->tag()); 167 | 168 | $sort_x = array_search($tag_x, self::FACT_ORDER) ?: PHP_INT_MAX; 169 | $sort_y = array_search($tag_y, self::FACT_ORDER) ?: PHP_INT_MAX; 170 | 171 | $cmp = $sort_x <=> $sort_y; 172 | if ($cmp !== 0) { 173 | return $cmp; 174 | } 175 | 176 | //fallback to original order within gedcom (e.g. for multiple names) 177 | return array_search($x, $factsArray) <=> array_search($y, $factsArray); 178 | }); 179 | 180 | return $facts; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ⚶ Vesta Shared Places (Webtrees Custom Module) 3 | 4 | This [webtrees](https://www.webtrees.net/) custom module supports shared places as level 0 GEDCOM objects, on the basis of the GEDCOM-L Addendum to the GEDCOM 5.5.1 specification. It displays data via the extended 'Facts and Events' tab, enhancing places with data obtained from the respective shared place. 5 | The project’s website is [cissee.de](https://cissee.de). 6 | 7 | ## Overview: Location data management 8 | 9 | See [here](https://github.com/vesta-webtrees-2-custom-modules/vesta_common/blob/master/docs/LocationData.md) for an overview of location data management in webtrees, including different use cases for shared places. 10 | 11 | ## Contents 12 | 13 | * [Features](#features) 14 | * [Gedcom-L Addendum](#gedcom) 15 | * [Download](#download) 16 | * [Installation](#installation) 17 | * [License](#license) 18 | 19 | ### Features 20 | 21 | * Shared places are handled as level 0 _LOC records, containing coordinates, notes, and media objects. 22 | * In combination with the Gov4Webtrees module, this module may also be used to manage GOV ids within GEDCOM data. 23 | * Shared places may be edited and viewed via an additional entry in the list menu. 24 | 25 |

    Screenshot

    26 | 27 | * Shared places also provide a place history tab, listing all events of specific types. This is particularly useful if you also use shared places for single buildings (e.g. farms), because it also shows the relationships between the respective individuals, thereby providing a 'line of succession' for the shared place. 28 | 29 |

    Screenshot

    30 | 31 | * On the (extended) facts and events tab, shared place data is displayed in addition to regular place data. 32 | 33 |

    Screenshot

    34 | 35 | * All shared place data is fully included in the gedcom exported by webtrees (and may also be imported from other sources supporting _LOC records), on the basis of the GEDCOM-L Addendum. 36 | * Location data is provided to other modules (e.g. for use in maps). 37 | 38 | * Shared places may be hierarchical, similar to regular places. Hierarchical relationships between shared places are modelled via explicit links (XREFs to higher-level shared places). This allows to model more complex relationships than via the place hierarchy itself, which uses comma-separated place name parts to indicate the hierarchy. When creating a shared place from a given place name, any missing higher-level shared places are created as well: 39 | 40 |

    Screenshot

    41 | 42 | * As hierarchical shared places have not been supported by earlier versions of this custom module, you may have to use a data fix in order to convert existing shared places (having comma-separated name parts), as a one-time migration. This will only affect shared place records of the respective GEDCOM file. 43 | 44 | * The Vesta Places and Pedigree Map module provides an extended place hierarchy list, which may be filtered to shared places. This is useful e.g. to show subordinate shared places. 45 | 46 | ### Gedcom-L Addendum
    47 | 48 | The Gedcom-L Addendum to the GEDCOM 5.5.1 specification is available [here](https://genealogy.net/GEDCOM/). It defines the following structure for top-level place records: 49 | 50 | ~~~~ 51 | LOCATION_RECORD:= 52 | 0 @@ _LOC {1:1} 53 | 1 NAME {1:M} 54 | 2 DATE {0:1} 55 | 2 ABBR {0:M} 56 | 3 TYPE {0:1} 57 | 2 LANG {0:1} 58 | 2 <> {0:M} 59 | 1 TYPE {0:M} 60 | 2 _GOVTYPE {0:1} 61 | 2 DATE {0:1} 62 | 2 <> {0:M} 63 | 1 _POST {0:M} 64 | 2 DATE {0:1} 65 | 2 <> {0:M} 66 | 1 _GOV {0:1} 67 | 1 MAP {0:1} 68 | 2 LATI {1:1} 69 | 2 LONG {1:1} 70 | 1 _MAIDENHEAD {0:1} 71 | 1 RELI {0:1} 72 | 1 EVEN [|] {0:M} 73 | 2 <> {0:1} 74 | 1 _LOC @@ {0:M} 75 | 2 TYPE {1:1} 76 | 2 DATE {0:1} 77 | 2 <> {0:M} 78 | 1 _DMGD {0:M} 79 | 2 DATE {0:1} 80 | 2 <> {0:M} 81 | 2 TYPE {1:1} 82 | 1 _AIDN {0:M} 83 | 2 DATE {0:1} 84 | 2 <> {0:M} 85 | 2 TYPE {1:1} 86 | 1 <> {0:M} 87 | 1 <> {0:M} 88 | 1 <> {0:M} 89 | 1 <> {0:1} 90 | ~~~~ 91 | 92 | ### Download 93 | 94 | * Current version: 2.2.4.1.0 95 | * Based on and tested with webtrees 2.2.4. Requires webtrees 2.2.1 or later. 96 | * Requires the ⚶ Vesta Common module ('vesta_common'). 97 | * Displays data via the ⚶ Vesta Facts and events module ('vesta_personal_facts'). 98 | * Provides location data to other custom modules. 99 | * Download the zip file, which includes all Vesta modules, [here](https://cissee.de/vesta.latest.zip). 100 | * Support, suggestions, feature requests: 101 | * Issues also via 102 | * Translations may be contributed via weblate: 103 | 104 | ### Installation 105 | 106 | * Unzip the files and copy the contents of the modules_v4 folder to the respective folder of your webtrees installation. All related modules are included in the zip file. It's safe to overwrite the respective directories if they already exist (they are bundled with other custom modules as well), as long as other custom models using these dependencies are also upgraded to their respective latest versions. 107 | * Enable the extended 'Facts and Events' module via Control Panel -> Modules -> All modules -> ⚶ Facts and Events. 108 | * Enable the main module via Control Panel -> Modules -> All modules -> ⚶ Shared Places. After that, you may configure some options. 109 | * Configure the visibility of the old and the extended 'Facts and Events' tab via Control Panel -> Modules -> Tabs (usually, you'll want to use only one of them. You may just disable the oringinal 'Facts and Events' module altogether). 110 | * Configure the visibility of the old 'Locations' and the extended 'Shared places' list via Control Panel -> Modules -> Lists (usually, you'll want to use only one of them. You may just disable the original 'Locations' module altogether). 111 | 112 | ### License 113 | 114 | * **vesta_shared_places: a webtrees custom module** 115 | * Copyright (C) 2019 – 2025 Richard Cissée 116 | * Derived from **webtrees** - Copyright 2022 webtrees development team. 117 | * French translations provided by Pierre Dousselin. 118 | * Dutch translations provided by TheDutchJewel. 119 | * Slovak translations provided by Ladislav Rosival. 120 | * Czech translations provided by Josef Prause. 121 | * Further translations contributed via weblate. 122 | 123 | This program is free software: you can redistribute it and/or modify 124 | it under the terms of the GNU General Public License as published by 125 | the Free Software Foundation, either version 3 of the License, or 126 | (at your option) any later version. 127 | 128 | This program is distributed in the hope that it will be useful, 129 | but WITHOUT ANY WARRANTY; without even the implied warranty of 130 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 131 | GNU General Public License for more details. 132 | 133 | You should have received a copy of the GNU General Public License 134 | along with this program. If not, see . 135 | -------------------------------------------------------------------------------- /resources/views/data-fix-options.phtml: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | 11 |

    12 | 13 | 14 |

    15 | 16 |
    17 | 20 | 21 |
    22 |
    23 | 28 | 31 |

    32 | 33 | 34 |

    35 | 36 | 37 |

    38 | 39 |

    40 |

    41 | 42 |

    43 | 44 | 45 |

    46 | 47 |

    48 |

    49 | 50 | 51 |

    52 | 53 | 54 |
    55 | 56 |
    57 | 62 | 65 |

    66 | 67 | 68 | 69 |

    70 |
    71 | 72 |
    73 | 78 | 81 |

    82 | 83 | 84 | 85 | 86 |

    87 |
    88 | 89 |
    90 | 95 | 98 |

    99 | 100 | 101 | 102 | 103 |

    104 | 105 | 106 |

    107 | 108 |

    109 |

    110 | 111 | 112 |

    113 | 114 |
    115 | 116 |
    117 | 122 | 125 |

    126 | 127 | 128 | 129 | 130 |

    131 | 132 | 133 |

    134 | 135 |

    136 |

    137 | 138 | 139 |

    140 | 141 |
    142 |
    143 | 144 |
    145 | -------------------------------------------------------------------------------- /patchedWebtrees/Elements/LanguageIdExt.php: -------------------------------------------------------------------------------- 1 | 89 | */ 90 | public static function values(): array 91 | { 92 | $values = [ 93 | //'' => '', 94 | 'Afrikaans' => (new LocaleAf()), 95 | 'Albanian' => (new LocaleSq()), 96 | 'Amharic' => (new LocaleAm()), 97 | 'Anglo-Saxon' => (new LocaleAng()), 98 | 'Arabic' => (new LocaleAr()), 99 | 'Armenian' => (new LocaleHy()), 100 | 'Assamese' => (new LocaleAs()), 101 | 'Belorusian' => (new LocaleBe()), 102 | 'Bengali' => (new LocaleBn()), 103 | //'Braj' => (new LocaleBra()), 104 | 'Bulgarian' => (new LocaleBg()), 105 | 'Burmese' => (new LocaleMy()), 106 | 'Cantonese' => (new LocaleYue()), 107 | 108 | //[RC] adjusted 109 | //'Catalan' => (new LocaleCaEsValencia()), 110 | //'Catalan_Spn' => (new LocaleCa()), 111 | 'Catalan' => (new LocaleCa()), 112 | 113 | 'Church-Slavic' => (new LocaleCu()), 114 | 'Czech' => (new LocaleCs()), 115 | 'Danish' => (new LocaleDa()), 116 | //'Dogri' => (new LocaleDoi()), 117 | 'Dutch' => (new LocaleNl()), 118 | 'English' => (new LocaleEn()), 119 | 'Esperanto' => (new LocaleEo()), 120 | 'Estonian' => (new LocaleEt()), 121 | 'Faroese' => (new LocaleFo()), 122 | 'Finnish' => (new LocaleFi()), 123 | 'French' => (new LocaleFr()), 124 | 'Georgian' => (new LocaleKa()), 125 | 'German' => (new LocaleDe()), 126 | 'Greek' => (new LocaleEl()), 127 | 'Gujarati' => (new LocaleGu()), 128 | 'Hawaiian' => (new LocaleHaw()), 129 | 'Hebrew' => (new LocaleHe()), 130 | 'Hindi' => (new LocaleHi()), 131 | 'Hungarian' => (new LocaleHu()), 132 | 'Icelandic' => (new LocaleIs()), 133 | 'Indonesian' => (new LocaleId()), 134 | 'Italian' => (new LocaleIt()), 135 | 'Japanese' => (new LocaleJa()), 136 | 'Kannada' => (new LocaleKn()), 137 | 'Khmer' => (new LocaleKm()), 138 | 'Konkani' => (new LocaleKok()), 139 | 'Korean' => (new LocaleKo()), 140 | //'Lahnda' => (new LocaleLah()), 141 | 'Lao' => (new LocaleLo()), 142 | 'Latvian' => (new LocaleLv()), 143 | 'Lithuanian' => (new LocaleLt()), 144 | 'Macedonian' => (new LocaleMk()), 145 | //'Maithili' => (new LocaleMai()), 146 | 'Malayalam' => (new LocaleMl()), 147 | //'Mandrin' => (new LocaleCmn()), 148 | //'Manipuri' => (new LocaleMni()), 149 | 'Marathi' => (new LocaleMr()), 150 | //'Mewari' => (new LocaleMtr()), 151 | //'Navaho' => (new LocaleNv()), 152 | 'Nepali' => (new LocaleNe()), 153 | 'Norwegian' => (new LocaleNn()), 154 | 'Oriya' => (new LocaleOr()), 155 | //'Pahari' => (new LocalePhr()), 156 | //'Pali' => (new LocalePi()), 157 | 'Panjabi' => (new LocalePa()), 158 | 'Persian' => (new LocaleFa()), 159 | 'Polish' => (new LocalePl()), 160 | 'Portuguese' => (new LocalePt()), 161 | //'Prakrit' => (new LocalePra()), 162 | 'Pusto' => (new LocalePs()), 163 | //'Rajasthani' => (new LocaleRaj()), 164 | 'Romanian' => (new LocaleRo()), 165 | 'Russian' => (new LocaleRu()), 166 | //'Sanskrit' => (new LocaleSa()), 167 | 'Serb' => (new LocaleSr()), 168 | //'Serbo_Croa' => (new LocaleHbs()), 169 | 'Slovak' => (new LocaleSk()), 170 | 'Slovene' => (new LocaleSl()), 171 | 'Spanish' => (new LocaleEs()), 172 | 'Swedish' => (new LocaleSv()), 173 | 'Tagalog' => (new LocaleTl()), 174 | 'Tamil' => (new LocaleTa()), 175 | 'Telugu' => (new LocaleTe()), 176 | 'Thai' => (new LocaleTh()), 177 | 'Tibetan' => (new LocaleBo()), 178 | 'Turkish' => (new LocaleTr()), 179 | 'Ukrainian' => (new LocaleUk()), 180 | 'Urdu' => (new LocaleUr()), 181 | 'Vietnamese' => (new LocaleVi()), 182 | //'Wendic' => (new LocaleWen()), 183 | 'Yiddish' => (new LocaleYi()), 184 | ]; 185 | 186 | return $values; 187 | } 188 | 189 | public static function valuesWithUpperCasedKeys(): array { 190 | $locales = LanguageIdExt::values(); 191 | 192 | $coll = new Collection($locales); 193 | 194 | $values = $coll 195 | ->mapWithKeys(static function (LocaleInterface $locale, string $key): array { 196 | return [strtoupper($key) => $locale]; 197 | }) 198 | ->all(); 199 | 200 | return $values; 201 | } 202 | 203 | } 204 | -------------------------------------------------------------------------------- /resources/views/js/webtreesExt.phtml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 223 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "2.0.13.0.0", 4 | "from": "2.0.12", 5 | "to": "2.0.18" 6 | }, 7 | { 8 | "version": "2.0.13.1.0", 9 | "from": "2.0.12", 10 | "to": "2.0.18" 11 | }, 12 | { 13 | "version": "2.0.15.0.0", 14 | "from": "2.0.12", 15 | "to": "2.0.18" 16 | }, 17 | { 18 | "version": "2.0.15.1.0", 19 | "from": "2.0.12", 20 | "to": "2.0.18" 21 | }, 22 | { 23 | "version": "2.0.15.2.0", 24 | "from": "2.0.12", 25 | "to": "2.0.18" 26 | }, 27 | { 28 | "version": "2.0.15.3.0", 29 | "from": "2.0.12", 30 | "to": "2.0.18" 31 | }, 32 | { 33 | "version": "2.0.15.4.0", 34 | "from": "2.0.12", 35 | "to": "2.0.18", 36 | "changelog": ["Option to set a reference date for the summary on the shared place page."] 37 | }, 38 | { 39 | "version": "2.0.16.0.0", 40 | "from": "2.0.12", 41 | "to": "2.0.18" 42 | }, 43 | { 44 | "version": "2.0.16.0.1", 45 | "from": "2.0.12", 46 | "to": "2.0.18", 47 | "changelog": ["Bugfix."] 48 | }, 49 | { 50 | "version": "2.0.16.0.2", 51 | "from": "2.0.12", 52 | "to": "2.0.18", 53 | "changelog": ["Bugfix."] 54 | }, 55 | { 56 | "version": "2.0.16.0.3", 57 | "from": "2.0.12", 58 | "to": "2.0.18" 59 | }, 60 | { 61 | "version": "2.0.16.0.4", 62 | "from": "2.0.12", 63 | "to": "2.0.18", 64 | "changelog": ["Small Bugfix."] 65 | }, 66 | { 67 | "version": "2.0.16.1.0", 68 | "from": "2.0.12", 69 | "to": "2.0.18" 70 | }, 71 | { 72 | "version": "2.0.16.1.1", 73 | "from": "2.0.12", 74 | "to": "2.0.18", 75 | "changelog": ["Display place names in the user's language if possible."] 76 | }, 77 | { 78 | "version": "2.0.16.2.0", 79 | "from": "2.0.12", 80 | "to": "2.0.18" 81 | }, 82 | { 83 | "version": "2.0.16.3.0", 84 | "from": "2.0.12", 85 | "to": "2.0.18", 86 | "changelog": ["Support shared places in general search."] 87 | }, 88 | { 89 | "version": "2.0.16.4.0", 90 | "from": "2.0.12", 91 | "to": "2.0.18" 92 | }, 93 | { 94 | "version": "2.0.16.5.0", 95 | "from": "2.0.12", 96 | "to": "2.0.18" 97 | }, 98 | { 99 | "version": "2.0.16.5.2", 100 | "from": "2.0.12", 101 | "to": "2.0.18" 102 | }, 103 | { 104 | "version": "2.0.16.6.0", 105 | "from": "2.0.12", 106 | "to": "2.0.18" 107 | }, 108 | { 109 | "version": "2.0.16.6.2", 110 | "from": "2.0.12", 111 | "to": "2.0.18" 112 | }, 113 | { 114 | "version": "2.0.17.0.0", 115 | "from": "2.0.12", 116 | "to": "2.0.18" 117 | }, 118 | { 119 | "version": "2.0.17.0.1", 120 | "from": "2.0.12", 121 | "to": "2.0.18" 122 | }, 123 | { 124 | "version": "2.0.17.1.0", 125 | "from": "2.0.12", 126 | "to": "2.0.18" 127 | }, 128 | { 129 | "version": "2.0.17.1.1", 130 | "from": "2.0.12", 131 | "to": "2.0.18", 132 | "changelog": ["Fix minor bugs related to map coordinates.","Show linked shared events on source page."] 133 | }, 134 | { 135 | "version": "2.0.19.0.0", 136 | "from": "2.0.12", 137 | "to": "2.0.20" 138 | }, 139 | { 140 | "version": "2.0.19.0.3", 141 | "from": "2.0.12", 142 | "to": "2.0.20" 143 | }, 144 | { 145 | "version": "2.0.19.0.5", 146 | "from": "2.0.12", 147 | "to": "2.0.20", 148 | "changelog": ["Handle hierarchical places when searching for shared places."] 149 | }, 150 | { 151 | "version": "2.0.19.1.0", 152 | "from": "2.0.12", 153 | "to": "2.0.20" 154 | }, 155 | { 156 | "version": "2.0.19.2.0", 157 | "from": "2.0.12", 158 | "to": "2.0.20" 159 | }, 160 | { 161 | "version": "2.0.22.0.0", 162 | "from": "2.0.12", 163 | "to": "2.0.23" 164 | }, 165 | { 166 | "version": "2.0.23+2.1.0-beta.2.0.0", 167 | "from": "2.0.12", 168 | "to": "2.1.0" 169 | }, 170 | { 171 | "version": "2.0.23+2.1.0-beta.2.0.1", 172 | "from": "2.0.12", 173 | "to": "2.1.0" 174 | }, 175 | { 176 | "version": "2.0.23+2.1.0-beta.2.0.2", 177 | "from": "2.0.12", 178 | "to": "2.1.0" 179 | }, 180 | { 181 | "version": "2.0.23+2.1.0-beta.2.1.0", 182 | "from": "2.0.12", 183 | "to": "2.1.0" 184 | }, 185 | { 186 | "version": "2.0.23+2.1.0-beta.2.2.0", 187 | "from": "2.0.12", 188 | "to": "2.1.0" 189 | }, 190 | { 191 | "version": "2.0.23+2.1.0-beta.2.3.0", 192 | "from": "2.0.12", 193 | "to": "2.1.0", 194 | "changelog": ["Preview of place history functionality (webtrees 2.1 branch only)."] 195 | }, 196 | { 197 | "version": "2.1.0.0.0", 198 | "from": "2.0.12", 199 | "to": "2.1.1" 200 | }, 201 | { 202 | "version": "2.1.0.1.0", 203 | "from": "2.0.12", 204 | "to": "2.1.1" 205 | }, 206 | { 207 | "version": "2.1.1.0.0", 208 | "from": "2.0.12", 209 | "to": "2.1.2", 210 | "changelog": ["Place history functionality."] 211 | }, 212 | { 213 | "version": "2.1.2.0.0", 214 | "from": "2.0.12", 215 | "to": "2.1.3" 216 | }, 217 | { 218 | "version": "2.1.2.1.0", 219 | "from": "2.0.12", 220 | "to": "2.1.3", 221 | "changelog": ["Bugfixes (create shared place functionality)."] 222 | }, 223 | { 224 | "version": "2.1.2.1.1", 225 | "from": "2.0.12", 226 | "to": "2.1.3" 227 | }, 228 | { 229 | "version": "2.1.4.0.0", 230 | "from": "2.1.4", 231 | "to": "2.1.6" 232 | }, 233 | { 234 | "version": "2.1.4.0.2", 235 | "from": "2.1.4", 236 | "to": "2.1.6" 237 | }, 238 | { 239 | "version": "2.1.4.1.1", 240 | "from": "2.1.4", 241 | "to": "2.1.6" 242 | }, 243 | { 244 | "version": "2.1.5.0.0", 245 | "from": "2.1.4", 246 | "to": "2.1.6" 247 | }, 248 | { 249 | "version": "2.1.6.0.0", 250 | "from": "2.1.4", 251 | "to": "2.1.7" 252 | }, 253 | { 254 | "version": "2.1.6.1.0", 255 | "from": "2.1.4", 256 | "to": "2.1.7", 257 | "changelog": ["Layout fix."] 258 | }, 259 | { 260 | "version": "2.1.6.1.1", 261 | "from": "2.1.4", 262 | "to": "2.1.7" 263 | }, 264 | { 265 | "version": "2.1.7.0.0", 266 | "from": "2.1.4", 267 | "to": "2.1.8" 268 | }, 269 | { 270 | "version": "2.1.7.0.2", 271 | "from": "2.1.4", 272 | "to": "2.1.8" 273 | }, 274 | { 275 | "version": "2.1.7.1.0", 276 | "from": "2.1.4", 277 | "to": "2.1.8" 278 | }, 279 | { 280 | "version": "2.1.7.1.1", 281 | "from": "2.1.4", 282 | "to": "2.1.8", 283 | "changelog": ["Small bugfix."] 284 | }, 285 | { 286 | "version": "2.1.8.0.0", 287 | "from": "2.1.8", 288 | "to": "2.1.14" 289 | }, 290 | { 291 | "version": "2.1.8.0.2", 292 | "from": "2.1.8", 293 | "to": "2.1.14" 294 | }, 295 | { 296 | "version": "2.1.9.0.0", 297 | "from": "2.1.8", 298 | "to": "2.1.14" 299 | }, 300 | { 301 | "version": "2.1.9.1.0", 302 | "from": "2.1.8", 303 | "to": "2.1.14" 304 | }, 305 | { 306 | "version": "2.1.9.1.0", 307 | "from": "2.1.8", 308 | "to": "2.1.14" 309 | }, 310 | { 311 | "version": "2.1.9.9.9", 312 | "from": "2.1.10", 313 | "to": "2.1.14" 314 | }, 315 | { 316 | "version": "2.1.10.0.0", 317 | "from": "2.1.10", 318 | "to": "2.1.14" 319 | }, 320 | { 321 | "version": "2.1.13.0.0", 322 | "from": "2.1.10", 323 | "to": "2.1.14" 324 | }, 325 | { 326 | "version": "2.1.15.0.0", 327 | "from": "2.1.10", 328 | "to": "2.1.17" 329 | }, 330 | { 331 | "version": "2.1.15.0.2", 332 | "from": "2.1.10", 333 | "to": "2.1.17", 334 | "changelog": ["Bugfix (use Shared Places without Personal Facts module)."] 335 | }, 336 | { 337 | "version": "2.1.15.0.3", 338 | "from": "2.1.10", 339 | "to": "2.1.17" 340 | }, 341 | { 342 | "version": "2.1.16.0.0", 343 | "from": "2.1.10", 344 | "to": "2.1.17" 345 | }, 346 | { 347 | "version": "2.1.16.1.0", 348 | "from": "2.1.10", 349 | "to": "2.1.17" 350 | }, 351 | { 352 | "version": "2.1.16.1.3", 353 | "from": "2.1.10", 354 | "to": "2.1.17" 355 | }, 356 | { 357 | "version": "2.1.16.1.5", 358 | "from": "2.1.10", 359 | "to": "2.1.17" 360 | }, 361 | { 362 | "version": "2.1.16.1.6", 363 | "from": "2.1.10", 364 | "to": "2.1.17" 365 | }, 366 | { 367 | "version": "2.1.16.2.0", 368 | "from": "2.1.10", 369 | "to": "2.1.17" 370 | }, 371 | { 372 | "version": "2.1.17.0.0", 373 | "from": "2.1.17", 374 | "to": "2.1.18" 375 | }, 376 | { 377 | "version": "2.1.17.1.0", 378 | "from": "2.1.17", 379 | "to": "2.1.18" 380 | }, 381 | { 382 | "version": "2.1.18.0.0", 383 | "from": "2.1.17", 384 | "to": "2.1.21" 385 | }, 386 | { 387 | "version": "2.1.18.0.1", 388 | "from": "2.1.17", 389 | "to": "2.1.21" 390 | }, 391 | { 392 | "version": "2.1.18.1.0", 393 | "from": "2.1.17", 394 | "to": "2.1.21" 395 | }, 396 | { 397 | "version": "2.1.18.2.0", 398 | "from": "2.1.17", 399 | "to": "2.1.21" 400 | }, 401 | { 402 | "version": "2.1.18.2.2", 403 | "from": "2.1.17", 404 | "to": "2.1.21" 405 | }, 406 | { 407 | "version": "2.1.19.0.0", 408 | "from": "2.1.17", 409 | "to": "2.1.21" 410 | }, 411 | { 412 | "version": "2.1.20.0.0", 413 | "from": "2.1.17", 414 | "to": "2.1.21" 415 | }, 416 | { 417 | "version": "2.1.20.1.0", 418 | "from": "2.1.17", 419 | "to": "2.1.21" 420 | }, 421 | { 422 | "version": "2.1.20.2.0", 423 | "from": "2.1.17", 424 | "to": "2.1.21" 425 | }, 426 | { 427 | "version": "2.2.0.0.0", 428 | "from": "2.1.17", 429 | "to": "2.2.1" 430 | }, 431 | { 432 | "version": "2.2.1.0.0", 433 | "from": "2.1.17", 434 | "to": "2.2.2" 435 | }, 436 | { 437 | "version": "2.2.1.1.0", 438 | "from": "2.1.17", 439 | "to": "2.2.2" 440 | }, 441 | { 442 | "version": "2.2.1.2.0", 443 | "from": "2.2.1", 444 | "to": "2.2.2" 445 | }, 446 | { 447 | "version": "2.2.1.3.0", 448 | "from": "2.2.1", 449 | "to": "2.2.2" 450 | }, 451 | { 452 | "version": "2.2.1.4.0", 453 | "from": "2.2.1", 454 | "to": "2.2.2" 455 | }, 456 | { 457 | "version": "2.2.1.5.0", 458 | "from": "2.2.1", 459 | "to": "2.2.2" 460 | }, 461 | { 462 | "version": "2.2.2.0.0", 463 | "from": "2.2.1", 464 | "to": "2.2.4" 465 | }, 466 | { 467 | "version": "2.2.3.0.0", 468 | "from": "2.2.1", 469 | "to": "2.2.4" 470 | }, 471 | { 472 | "version": "2.2.4.0.0", 473 | "from": "2.2.1", 474 | "to": "2.2.5" 475 | }, 476 | { 477 | "version": "2.2.4.1.0", 478 | "from": "2.2.1", 479 | "to": "2.2.5" 480 | } 481 | ] 482 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/CreateSharedPlaceAction.php: -------------------------------------------------------------------------------- 1 | tree(); 35 | 36 | $params = (array) $request->getParsedBody(); 37 | $useHierarchy = (bool) ($params['useHierarchy'] ?? false); 38 | $name = $params['shared-place-name']; 39 | $govId = $params['shared-place-govId'] ?? ''; 40 | 41 | // Fix whitespace 42 | $name = trim(preg_replace('/\s+/', ' ', $name)); 43 | 44 | if ($useHierarchy) { 45 | $ref = $this->createIfRequired($name, $govId, $tree); 46 | $record = $ref->record(); 47 | 48 | if ($ref->created() === 0) { 49 | return response([ 50 | 'html' => view('modals/record-created', [ 51 | 'title' => I18N::translate('The shared place %s already exists.', $record->fullName()), 52 | 'name' => $record->fullName(), 53 | 'url' => $record->url(), 54 | ]), 55 | ], 409); 56 | } else { 57 | $html = ''; 58 | if ($ref->created() === 2) { 59 | $html = ' ' . I18N::translate(' (Note: A higher-level shared place has also been created)'); 60 | } else if ($ref->created() > 2) { 61 | $html = ' ' . I18N::translate(' (Note: %s higher-level shared places have also been created)', $ref->created() - 1); 62 | } 63 | 64 | // value and text and title are for autocomplete 65 | // html is for interactive modals 66 | return response([ 67 | 'value' => '@' . $record->xref() . '@', 68 | 'text' => view('selects/location', [ 69 | 'location' => $record, 70 | ]), 71 | //cf TomSelectSharedPlace, in this case same as text! 72 | 'title' => view('selects/location', [ 73 | 'location' => $record, 74 | ]), 75 | 'html' => view('modals/record-created', [ 76 | 'title' => I18N::translate('The shared place %s has been created.', $record->fullName()), 77 | 'name' => $record->fullName() . $html, 78 | 'url' => $record->url(), 79 | ]), 80 | ]); 81 | } 82 | } 83 | 84 | //else (no hierarchy) 85 | 86 | $privacy_restriction = Requests::getString($request, 'privacy-restriction'); 87 | $edit_restriction = Requests::getString($request, 'edit-restriction'); 88 | 89 | $gedcom = "0 @@ _LOC\n1 NAME " . $name; 90 | 91 | if ($govId != '') { 92 | $gedcom .= "\n1 _GOV " . $govId; 93 | } 94 | 95 | if (in_array($privacy_restriction, [ 96 | 'none', 97 | 'privacy', 98 | 'confidential', 99 | ])) { 100 | $gedcom .= "\n1 RESN " . $privacy_restriction; 101 | } 102 | 103 | if (in_array($edit_restriction, ['locked'])) { 104 | $gedcom .= "\n1 RESN " . $edit_restriction; 105 | } 106 | 107 | $record = $tree->createRecord($gedcom); //returns GedcomRecord 108 | $record = Registry::locationFactory()->make($record->xref(), $tree); //we need Location for proper names! 109 | //FlashMessages::addMessage(I18N::translate('The shared place %s has been created.', $name), 'info'); 110 | // id and text are for select2 / autocomplete 111 | // html is for interactive modals 112 | return response([ 113 | 'id' => $record->xref(), 114 | 'text' => view('selects/location', [ 115 | 'location' => $record, 116 | ]), 117 | //cf TomSelectSharedPlace, in this case same as text! 118 | 'title' => view('selects/location', [ 119 | 'location' => $record, 120 | ]), 121 | 'html' => view('modals/record-created', [ 122 | 'title' => I18N::translate('The shared place %s has been created.', $name), 123 | 'name' => $record->fullName(), 124 | 'url' => $record->url(), 125 | ]), 126 | ]); 127 | } 128 | 129 | public function createIfRequired( 130 | string $placeGedcomName, 131 | string $govId, 132 | Tree $tree, 133 | bool $simulate = false, 134 | ?SharedPlacesModule $enhanceWithGlobalData = null, 135 | bool $onlyIfGlobalDataAvailable = false): ?SharedPlaceRef { 136 | 137 | $parts = SharedPlace::placeNameParts($placeGedcomName); 138 | $tail = SharedPlace::placeNamePartsTail($parts); 139 | $head = reset($parts); 140 | 141 | //hacky - should we even support this here? 142 | if ($enhanceWithGlobalData !== null) { 143 | $useHierarchy = boolval($enhanceWithGlobalData->getPreference('USE_HIERARCHY', '1')); 144 | 145 | if (!$useHierarchy) { 146 | $head = $placeGedcomName; 147 | $tail = ''; 148 | } 149 | } 150 | 151 | //if the place exists (with hierarchy), just return 152 | /* @var $searchService SearchServiceExt */ 153 | $searchService = \Vesta\VestaUtils::get(SearchServiceExt::class); 154 | $sharedPlace = $searchService->searchLocationsInPlace(new Place($placeGedcomName, $tree))->first(); 155 | if ($sharedPlace !== null) { 156 | return new SharedPlaceRef($sharedPlace, true, 0, null); 157 | } 158 | 159 | //otherwise create (including missing parents) 160 | 161 | $gedcom = "0 @@ _LOC\n1 NAME " . $head; 162 | 163 | $enhancedWithGlobalData = false; 164 | 165 | if ($govId != '') { 166 | $gedcom .= "\n1 _GOV " . $govId; 167 | } else if ($enhanceWithGlobalData !== null) { 168 | $plac2GovSupporters = $enhanceWithGlobalData->getPlac2GovSupporters($tree); 169 | 170 | if (sizeof($plac2GovSupporters) > 0) { 171 | foreach ($plac2GovSupporters as $plac2GovSupporter) { 172 | $gov = $plac2GovSupporter->plac2gov(PlaceStructure::fromName($placeGedcomName, $tree)); 173 | if ($gov !== null) { 174 | $gedcom .= "\n1 _GOV " . $gov->getId(); 175 | $enhancedWithGlobalData = true; 176 | break; 177 | } 178 | } 179 | } 180 | } 181 | 182 | if ($enhanceWithGlobalData !== null) { 183 | $ll = $enhanceWithGlobalData->getLatLon($placeGedcomName); 184 | 185 | if ($ll !== null) { 186 | $map_lati = ($ll[0] < 0) ? "S" . str_replace('-', '', (string) $ll[0]) : "N" . $ll[0]; 187 | $map_long = ($ll[1] < 0) ? "W" . str_replace('-', '', (string) $ll[1]) : "E" . $ll[1]; 188 | $gedcom .= "\n1 MAP\n2 LATI " . $map_lati . "\n2 LONG " . $map_long; 189 | $enhancedWithGlobalData = true; 190 | } 191 | } 192 | 193 | if ($onlyIfGlobalDataAvailable && !$enhancedWithGlobalData) { 194 | return null; 195 | } 196 | 197 | ///////////////////////////////////////////////////////////////////////// 198 | //start to actually change something! 199 | 200 | $ref = null; 201 | if ($tail !== '') { 202 | //missing parents have to be created regardless of their own $onlyIfGlobalDataAvailable! 203 | $ref = $this->createIfRequired($tail, '', $tree, $simulate, $enhanceWithGlobalData); 204 | } 205 | 206 | if ($ref !== null) { 207 | $gedcom .= "\n1 _LOC @" . $ref->record()->xref() . "@"; 208 | $gedcom .= "\n2 TYPE POLI"; 209 | } 210 | 211 | if (!$simulate) { 212 | $record = $tree->createRecord($gedcom); //returns GedcomRecord 213 | } 214 | $newXref = 'NX_' . CreateSharedPlaceAction::generateRandomString(16); 215 | if (!$simulate) { 216 | $newXref = $record->xref(); 217 | } 218 | 219 | //we need Location for proper names! 220 | //and we must check() in order to update place links 221 | /** @var SharedPlace $record */ 222 | 223 | $record = Registry::locationFactory()->make($newXref, $tree, $gedcom); 224 | $record->check(); 225 | 226 | $count = 1; 227 | if ($ref !== null) { 228 | $count += $ref->created(); 229 | } 230 | return new SharedPlaceRef($record, false, $count, $ref); 231 | } 232 | 233 | //Uuid::uuid4() is to long for XREF (max length 20) 234 | //https://stackoverflow.com/questions/4356289/php-random-string-generator 235 | public static function generateRandomString($length = 10) { 236 | $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 237 | $charactersLength = strlen($characters); 238 | $randomString = ''; 239 | for ($i = 0; $i < $length; $i++) { 240 | $randomString .= $characters[rand(0, $charactersLength - 1)]; 241 | } 242 | return $randomString; 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /patchedWebtrees/PlaceViaSharedPlace.php: -------------------------------------------------------------------------------- 1 | actual = $actual; 53 | $this->asAdditionalParticipant = $asAdditionalParticipant; 54 | $this->urls = $urls; 55 | $this->module = $module; 56 | $this->sharedPlaces = $sharedPlaces; 57 | } 58 | 59 | //Speedup 60 | //more efficient than $this->actual->url() when calling for lots of places 61 | //this should be in webtrees, e.g. via caching result of 'findByComponent' or even 'Auth::accessLevel' 62 | public function url(): string { 63 | return $this->urls->url($this->actual); 64 | } 65 | 66 | public function gedcomName(): string { 67 | return $this->actual->gedcomName(); 68 | } 69 | 70 | public function placeName(): string { 71 | $uniqueSharedPlaces = boolval($this->module->getPreference('UNIQUE_SP_IN_HIERARCHY', '0')); 72 | 73 | if ($uniqueSharedPlaces && !$this->asAdditionalParticipant) { 74 | //by design only current names (exclude historical names!) 75 | $place_name = $this->sharedPlaces 76 | ->flatMap(function (SharedPlace $sharedPlace): array { 77 | return $sharedPlace->namesNNAt(null); 78 | }) 79 | ->reduce(function ($carry, $item): string { 80 | return ($carry === "")?$item:$carry . " | " . $item; 81 | }, ""); 82 | 83 | return '' . e($place_name) . ''; 84 | } 85 | 86 | return $this->actual->placeName(); 87 | } 88 | 89 | //cf DefaultPlaceWithinHierarchy, but map to SharedPlace/PlaceViaSharedPlace directly from query 90 | public function getChildPlacesCacheIds( 91 | Place $place): Collection { 92 | 93 | $self = $this; 94 | 95 | if ($place->gedcomName() !== '') { 96 | $parent_text = Gedcom::PLACE_SEPARATOR . $place->gedcomName(); 97 | } else { 98 | $parent_text = ''; 99 | } 100 | 101 | $tree = $place->tree(); 102 | 103 | $place2sharedPlaceMap = DB::table('places') 104 | ->where('p_file', '=', $tree->id()) 105 | ->where('p_parent_id', '=', $place->id()) 106 | ->join('placelinks', static function (JoinClause $join): void { 107 | $join 108 | ->on('pl_file', '=', 'p_file') 109 | ->on('pl_p_id', '=', 'p_id'); 110 | }) 111 | ->join('other', static function (JoinClause $join): void { 112 | $join 113 | ->on('o_file', '=', 'pl_file') 114 | ->on('o_id', '=', 'pl_gid'); 115 | }) 116 | ->where('o_type', '=', '_LOC') 117 | ->get() 118 | ->map(function (stdClass $row) use ($parent_text, $tree): array { 119 | $place = new Place($row->p_place . $parent_text, $tree); 120 | $id = $row->p_id; 121 | Registry::cache()->array()->remember('place-' . $place->gedcomName(), function () use ($id): int {return $id;}); 122 | 123 | $sharedPlace = Registry::locationFactory()->mapper($tree)($row); 124 | return ["actual" => $place, "record" => $sharedPlace]; 125 | }) 126 | //must filter as in SearchServiceExt::searchLocationsInPlace 127 | ->filter(function ($item): bool { 128 | //include only if name matches! 129 | $names = new Collection($item["record"]->namesAsPlacesAt(GedcomDateInterval::createEmpty())); 130 | return $names->has($item["actual"]->id()); 131 | }); 132 | 133 | if ($place2sharedPlaceMap->isEmpty()) { 134 | return new Collection(); 135 | } 136 | 137 | $uniqueSharedPlaces = boolval($this->module->getPreference('UNIQUE_SP_IN_HIERARCHY', '0')); 138 | 139 | //if $asAdditionalParticipant, we just need the additional data (map coordinates) 140 | //and do not evaluate $uniqueSharedPlaces 141 | if ($uniqueSharedPlaces && !$this->asAdditionalParticipant) { 142 | $place2sharedPlaceMap = $place2sharedPlaceMap 143 | ->mapToGroups(static function ($item): array { 144 | /** @var SharedPlace $sharedPlace */ 145 | $sharedPlace = $item["record"]; 146 | return [$sharedPlace->xref() => $item]; 147 | }) 148 | ->map(static function (Collection $groupedItems): array { 149 | $first = $groupedItems->first(); 150 | /** @var SharedPlace $sharedPlace */ 151 | $sharedPlace = $first["record"]; 152 | 153 | //which place to use? follow order from shared place 154 | $place = null; 155 | foreach ($sharedPlace->namesAsPlacesAt(GedcomDateInterval::createEmpty()) as $placeViaSharedPlace) { 156 | foreach ($groupedItems as $groupedItem) { 157 | if ($placeViaSharedPlace->id() === $groupedItem["actual"]->id()) { 158 | $place = $groupedItem["actual"]; 159 | break 2; 160 | } 161 | } 162 | } 163 | 164 | if ($place === null) { 165 | throw new Exception("unexpected null place!"); 166 | } 167 | return ["actual" => $place, "record" => $sharedPlace]; 168 | }); 169 | } 170 | 171 | $childPlaces = $place2sharedPlaceMap 172 | ->mapToGroups(static function ($item): array { 173 | $place = $item["actual"]; 174 | return [$place->id() => $item]; 175 | }) 176 | ->map(static function (Collection $groupedItems) use ($self): PlaceViaSharedPlace { 177 | $first = $groupedItems->first(); 178 | $sharedPlaces = $groupedItems 179 | ->map(static function (array $inner): SharedPlace { 180 | return $inner["record"]; 181 | }) 182 | ->unique(function (SharedPlace $sharedPlace): string { 183 | return $sharedPlace->xref(); 184 | }); 185 | 186 | return new PlaceViaSharedPlace( 187 | $first["actual"], 188 | $self->asAdditionalParticipant, 189 | $self->urls, 190 | $sharedPlaces, 191 | $self->module); 192 | }) 193 | ->sort(static function (PlaceViaSharedPlace $x, PlaceViaSharedPlace $y): int { 194 | return strtolower($x->gedcomName()) <=> strtolower($y->gedcomName()); 195 | }) 196 | ->mapWithKeys(static function (PlaceViaSharedPlace $place): array { 197 | return [$place->id() => $place]; 198 | }); 199 | 200 | return $childPlaces; 201 | } 202 | 203 | public function getChildPlaces(): array { 204 | $ret = $this 205 | ->getChildPlacesCacheIds($this->actual) 206 | ->toArray(); 207 | 208 | return $ret; 209 | } 210 | 211 | public function id(): int { 212 | return $this->actual->id(); 213 | } 214 | 215 | public function tree(): Tree { 216 | return $this->actual->tree(); 217 | } 218 | 219 | public function fullName(bool $link = false): string { 220 | return $this->actual->fullName($link); 221 | } 222 | 223 | public function searchIndividualsInPlace(): Collection { 224 | return SharedPlace::linkedIndividualsRecords($this->sharedPlaces); 225 | } 226 | 227 | public function countIndividualsInPlace(): int { 228 | return SharedPlace::linkedIndividualsCount($this->sharedPlaces); 229 | } 230 | 231 | public function searchFamiliesInPlace(): Collection { 232 | return SharedPlace::linkedFamiliesRecords($this->sharedPlaces); 233 | } 234 | 235 | public function countFamiliesInPlace(): int { 236 | return SharedPlace::linkedFamiliesCount($this->sharedPlaces); 237 | } 238 | 239 | protected function initLatLon(): ?MapCoordinates { 240 | $useIndirectLinks = boolval($this->module->getPreference('INDIRECT_LINKS', '1')); 241 | 242 | if (!$useIndirectLinks) { 243 | //check shared places directly, they won't be checked via plac2map 244 | foreach ($this->sharedPlaces as $sharedPlace) { 245 | /* @var $sharedPlace SharedPlace */ 246 | 247 | $locReference = new LocReference($sharedPlace->xref(), $sharedPlace->tree(), new Trace('')); 248 | $mapCoordinates = FunctionsPlaceUtils::loc2map($this->module, $locReference); 249 | if ($mapCoordinates !== null) { 250 | return $mapCoordinates; 251 | } 252 | } 253 | } 254 | 255 | $ps = $this->placeStructure(); 256 | if ($ps === null) { 257 | return null; 258 | } 259 | return FunctionsPlaceUtils::plac2map($this->module, $ps, false); 260 | } 261 | 262 | public function getLatLon(): ?MapCoordinates { 263 | if (!$this->latLonInitialized) { 264 | $this->latLon = $this->initLatLon(); 265 | $this->latLonInitialized = true; 266 | } 267 | 268 | return $this->latLon; 269 | } 270 | 271 | public function latitude(): ?float { 272 | //we don't go up the hierarchy here - there may be more than one parent! 273 | 274 | $lati = null; 275 | if ($this->getLatLon() !== null) { 276 | $lati = $this->getLatLon()->getLati(); 277 | } 278 | if ($lati === null) { 279 | return null; 280 | } 281 | 282 | $gedcom_service = new GedcomService(); 283 | return $gedcom_service->readLatitude($lati); 284 | } 285 | 286 | public function longitude(): ?float { 287 | //we don't go up the hierarchy here - there may be more than one parent! 288 | 289 | $long = null; 290 | if ($this->getLatLon() !== null) { 291 | $long = $this->getLatLon()->getLong(); 292 | } 293 | if ($long === null) { 294 | return null; 295 | } 296 | 297 | $gedcom_service = new GedcomService(); 298 | return $gedcom_service->readLongitude($long); 299 | } 300 | 301 | public function icon(): string { 302 | return ''; 303 | } 304 | 305 | public function boundingRectangleWithChildren(array $children): array 306 | { 307 | /* 308 | if (top-level) { 309 | //why doesn't original impl calculate bounding rectangle for world? Too expensive? 310 | return [[-180.0, -90.0], [180.0, 90.0]]; 311 | } 312 | */ 313 | 314 | $latitudes = []; 315 | $longitudes = []; 316 | 317 | if ($this->latitude() !== null) { 318 | $latitudes[] = $this->latitude(); 319 | } 320 | if ($this->longitude() !== null) { 321 | $longitudes[] = $this->longitude(); 322 | } 323 | 324 | foreach ($children as $child) { 325 | if ($child->latitude() !== null) { 326 | $latitudes[] = $child->latitude(); 327 | } 328 | if ($child->longitude() !== null) { 329 | $longitudes[] = $child->longitude(); 330 | } 331 | } 332 | 333 | if ((count($latitudes) === 0) || (count($longitudes) === 0)) { 334 | return [[-180.0, -90.0], [180.0, 90.0]]; 335 | } 336 | 337 | $latiMin = (new Collection($latitudes))->min(); 338 | $longMin = (new Collection($longitudes))->min(); 339 | $latiMax = (new Collection($latitudes))->max(); 340 | $longMax = (new Collection($longitudes))->max(); 341 | 342 | //never zoom in too far (in particular if there is only one place, but also if the places are close together) 343 | $latiSpread = $latiMax - $latiMin; 344 | if ($latiSpread < 1) { 345 | $latiMin -= (1 - $latiSpread)/2; 346 | $latiMax += (1 - $latiSpread)/2; 347 | } 348 | 349 | $longSpread = $longMax - $longMin; 350 | if ($longSpread < 1) { 351 | $longMin -= (1 - $longSpread)/2; 352 | $longMax += (1 - $longSpread)/2; 353 | } 354 | 355 | return [[$latiMin, $longMin], [$latiMax, $longMax]]; 356 | } 357 | 358 | public function placeStructure(): ?PlaceStructure { 359 | return PlaceStructure::fromPlace($this->actual); 360 | } 361 | 362 | public function additionalLinksHtmlBeforeName(): string { 363 | $html = ''; 364 | if ($this->module !== null) { 365 | foreach ($this->sharedPlaces as $sharedPlace) { 366 | $html .= $this->module->getLinkForSharedPlace($sharedPlace); 367 | } 368 | } 369 | 370 | return $html; 371 | } 372 | 373 | public function links(): Collection { 374 | return $this->urls->links($this->actual); 375 | } 376 | 377 | public function parent(): PlaceWithinHierarchy { 378 | return $this->module->findPlace($this->actual->parent()->id(), $this->actual->tree(), $this->urls); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /patchedWebtrees/Services/SearchServiceExt.php: -------------------------------------------------------------------------------- 1 | tree_service = $tree_service; 38 | } 39 | 40 | public function searchLocationsInPlace(Place $place): Collection { 41 | //it may seem more efficient to filter to roots via LocGraph, 42 | //but there are edge cases where that isn't correct (if a shared places maps to "A, B" as well as "B") 43 | 44 | return $this->searchLocationHierarchiesInPlace($place) 45 | ->filter(function (SharedPlace $sharedPlace) use ($place): bool { 46 | //include only if name matches! 47 | $names = new Collection($sharedPlace->namesAsPlacesAt(GedcomDateInterval::createEmpty())); 48 | return $names->has($place->id()); 49 | }); 50 | } 51 | 52 | public function searchLocationsInPlaces(Tree $tree, Collection $places): Collection { 53 | //it may seem more efficient to filter to roots via LocGraph, 54 | //but there are edge cases where that isn't correct (if a shared places maps to "A, B" as well as "B") 55 | 56 | return $this->searchLocationHierarchiesInPlaces($tree, $places) 57 | ->filter(function (SharedPlace $sharedPlace) use ($places): bool { 58 | //include only if name matches! 59 | $names = new Collection($sharedPlace->namesAsPlacesAt(GedcomDateInterval::createEmpty())); 60 | 61 | $anyHas = $places->first(static function ($place) use ($names) { 62 | return $names->has($place->id()); 63 | }); 64 | return ($anyHas != null); 65 | }); 66 | } 67 | 68 | //[2021/03] now that placelinks includes all child LOCs as well (placelinks requires these to prevent orphaning), 69 | //this function returns more than usually intended: cf searchLocationsInPlace 70 | public function searchLocationHierarchiesInPlace(Place $place): Collection { 71 | return DB::table('other') 72 | ->join('placelinks', static function (JoinClause $query) { 73 | $query 74 | ->on('other.o_file', '=', 'placelinks.pl_file') 75 | ->on('other.o_id', '=', 'placelinks.pl_gid'); 76 | }) 77 | ->where('o_type', '=', '_LOC') 78 | ->where('o_file', '=', $place->tree()->id()) 79 | ->where('pl_p_id', '=', $place->id()) 80 | ->select(['other.*']) 81 | ->get() 82 | //->each($this->rowLimiter()) //unlikely to be relevant anyway 83 | ->map($this->locationRowMapper()) 84 | ->filter(GedcomRecord::accessFilter()); 85 | } 86 | 87 | public function searchLocationHierarchiesInPlaces(Tree $tree, Collection $places): Collection { 88 | return DB::table('other') 89 | ->join('placelinks', static function (JoinClause $query) { 90 | $query 91 | ->on('other.o_file', '=', 'placelinks.pl_file') 92 | ->on('other.o_id', '=', 'placelinks.pl_gid'); 93 | }) 94 | ->where('o_type', '=', '_LOC') 95 | ->where('o_file', '=', $tree->id()) 96 | ->whereIn('pl_p_id', $places->map(static function (Place $place): int { 97 | return $place->id(); 98 | })->all()) 99 | ->select(['other.*']) 100 | ->get() 101 | //->each($this->rowLimiter()) //unlikely to be relevant anyway 102 | ->map($this->locationRowMapper()) 103 | ->filter(GedcomRecord::accessFilter()); 104 | } 105 | 106 | /** 107 | * Search for shared places. 108 | * 109 | * @param Tree[] $trees 110 | * @param string[] $search 111 | * @param int $offset 112 | * @param int $limit 113 | * 114 | * @return Collection|Location[] 115 | */ 116 | public function searchLocations(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection { 117 | $query = DB::table('other') 118 | ->where('o_type', '=', '_LOC'); 119 | 120 | $this->whereTrees($query, 'o_file', $trees); 121 | $this->whereSearch($query, 'o_gedcom', $search); 122 | 123 | return $this->paginateQuery( 124 | $query, 125 | $this->locationRowMapper(), 126 | GedcomRecord::accessFilter(), 127 | $offset, 128 | $limit); 129 | } 130 | 131 | /** 132 | * Search for shared places. 133 | * 134 | * @param Tree[] $trees 135 | * @param string[] $search 136 | * @param int $offset 137 | * @param int $limit 138 | * 139 | * @return Collection|Location[] 140 | */ 141 | public function searchLocationsEOL(array $trees, array $search, int $offset = 0, int $limit = PHP_INT_MAX): Collection { 142 | $query = DB::table('other') 143 | ->where('o_type', '=', '_LOC'); 144 | 145 | $this->whereTrees($query, 'o_file', $trees); 146 | $this->whereSearchEOL($query, 'o_gedcom', $search); 147 | 148 | return $this->paginateQuery($query, $this->locationRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 149 | } 150 | 151 | public function searchTopLevelLocations(array $trees, int $offset = 0, int $limit = PHP_INT_MAX): Collection { 152 | //not useful because a location may have links to parent locations for some dates 153 | //while being a top-level location at some other date 154 | /* 155 | $query = DB::table('other') 156 | ->leftJoin('link', static function (JoinClause $join): void { 157 | $join 158 | ->on('l_file', '=', 'o_file') 159 | ->on('l_from', '=', 'o_id') 160 | ->where('l_type', '=', '_LOC'); 161 | }) 162 | ->whereNull('l_from') 163 | ->where('o_type', '=', '_LOC'); 164 | 165 | $this->whereTrees($query, 'o_file', $trees); 166 | */ 167 | 168 | //a top-level location is a location linked to at least one top-level (i.e. parentless) place 169 | /* @var $query Builder */ 170 | $query = DB::table('other') 171 | ->join('placelinks', static function (JoinClause $join): void { 172 | $join 173 | ->on('pl_file', '=', 'o_file') 174 | ->on('pl_gid', '=', 'o_id'); 175 | }) 176 | ->join('places', static function (JoinClause $join): void { 177 | $join 178 | ->on('p_id', '=', 'pl_p_id'); 179 | }) 180 | ->where('p_parent_id', '=', 0) 181 | ->where('o_type', '=', '_LOC'); 182 | 183 | $this->whereTrees($query, 'o_file', $trees); 184 | $query->distinct(); 185 | $query->select(['o_id', 'o_file', 'o_type', 'o_gedcom']); //must select explicitly, otherwise '*' which messes up the distinct 186 | 187 | return $this->paginateQuery($query, $this->locationRowMapper(), GedcomRecord::accessFilter(), $offset, $limit); 188 | } 189 | 190 | private function locationRowMapper(): Closure { 191 | return function (stdClass $row): Location { 192 | /* 193 | try { 194 | $tree = $this->tree_service->find((int) $row->o_file); 195 | } catch (\Exception $ex) { 196 | error_log("private tree? " . $row->o_file); 197 | error_log(print_r($this->tree_service->all(), true)); 198 | throw new \Exception("private tree? " . $row->o_file); 199 | } 200 | */ 201 | $tree = $this->tree_service->find((int) $row->o_file); 202 | 203 | return Registry::locationFactory()->mapper($tree)($row); 204 | }; 205 | } 206 | 207 | /** 208 | * Paginate a search query. 209 | * 210 | * @param Builder $query Searches the database for the desired records. 211 | * @param Closure $row_mapper Converts a row from the query into a record. 212 | * @param Closure $row_filter 213 | * @param int $offset Skip this many rows. 214 | * @param int $limit Take this many rows. 215 | * 216 | * @return Collection 217 | */ 218 | private function paginateQuery(Builder $query, Closure $row_mapper, Closure $row_filter, int $offset, int $limit): Collection { 219 | $collection = new Collection(); 220 | 221 | foreach ($query->cursor() as $row) { 222 | $record = $row_mapper($row); 223 | // If the object has a method "canShow()", then use it to filter for privacy. 224 | if ($row_filter($record)) { 225 | if ($offset > 0) { 226 | $offset--; 227 | } else { 228 | if ($limit > 0) { 229 | $collection->push($record); 230 | } 231 | 232 | $limit--; 233 | 234 | if ($limit === 0) { 235 | break; 236 | } 237 | } 238 | } 239 | } 240 | 241 | return $collection; 242 | } 243 | 244 | /** 245 | * Apply search filters to a SQL query column. Apply collation rules to MySQL. 246 | * 247 | * @param Builder $query 248 | * @param Expression|string $field 249 | * @param string[] $search_terms 250 | */ 251 | private function whereSearch(Builder $query, $field, array $search_terms): void { 252 | if ($field instanceof Expression) { 253 | $field = $field->getValue(); 254 | } 255 | 256 | foreach ($search_terms as $search_term) { 257 | $query->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_') . '%'); 258 | } 259 | } 260 | 261 | private function whereSearchEOL(Builder $query, $field, array $search_terms): void { 262 | if ($field instanceof Expression) { 263 | $field = $field->getValue(); 264 | } 265 | 266 | foreach ($search_terms as $search_term) { 267 | //issue #122 268 | //this doesn't nest the disjunction as intended! 269 | /* 270 | $query 271 | ->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term . "\n", '\\%_') . '%') //EOL 272 | ->orWhere(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_')); //EOL via end of entire entry 273 | */ 274 | $query->where(static function (Builder $q) use ($field, $search_term): void { 275 | $q 276 | ->where(new Expression($field), 'LIKE', '%' . addcslashes($search_term . "\n", '\\%_') . '%') //EOL 277 | ->orWhere(new Expression($field), 'LIKE', '%' . addcslashes($search_term, '\\%_')); //EOL via end of entire entry 278 | }); 279 | } 280 | } 281 | 282 | /** 283 | * @param Builder $query 284 | * @param string $tree_id_field 285 | * @param Tree[] $trees 286 | */ 287 | private function whereTrees(Builder $query, string $tree_id_field, array $trees): void { 288 | $tree_ids = array_map(function (Tree $tree) { 289 | return $tree->id(); 290 | }, $trees); 291 | 292 | $query->whereIn($tree_id_field, $tree_ids); 293 | } 294 | 295 | //same as main, but 296 | //a) handle search strings 'A, B' (main only handles 'A,B') 297 | //b) search first for 'startingWith' 298 | /** 299 | * Search for places. 300 | * 301 | * @param Tree $tree 302 | * @param string $search 303 | * @param int $offset 304 | * @param int $limit 305 | * 306 | * @return Collection 307 | */ 308 | public function searchPlaces( 309 | Tree $tree, 310 | string $search, 311 | bool $startsWith = false, 312 | int $offset = 0, 313 | int $limit = PHP_INT_MAX): Collection { 314 | 315 | $query = DB::table('places AS p0') 316 | ->where('p0.p_file', '=', $tree->id()) 317 | ->leftJoin('places AS p1', 'p1.p_id', '=', 'p0.p_parent_id') 318 | ->leftJoin('places AS p2', 'p2.p_id', '=', 'p1.p_parent_id') 319 | ->leftJoin('places AS p3', 'p3.p_id', '=', 'p2.p_parent_id') 320 | ->leftJoin('places AS p4', 'p4.p_id', '=', 'p3.p_parent_id') 321 | ->leftJoin('places AS p5', 'p5.p_id', '=', 'p4.p_parent_id') 322 | ->leftJoin('places AS p6', 'p6.p_id', '=', 'p5.p_parent_id') 323 | ->leftJoin('places AS p7', 'p7.p_id', '=', 'p6.p_parent_id') 324 | ->leftJoin('places AS p8', 'p8.p_id', '=', 'p7.p_parent_id') 325 | ->orderBy('p0.p_place') 326 | ->orderBy('p1.p_place') 327 | ->orderBy('p2.p_place') 328 | ->orderBy('p3.p_place') 329 | ->orderBy('p4.p_place') 330 | ->orderBy('p5.p_place') 331 | ->orderBy('p6.p_place') 332 | ->orderBy('p7.p_place') 333 | ->orderBy('p8.p_place') 334 | ->select([ 335 | 'p0.p_place AS place0', 336 | 'p1.p_place AS place1', 337 | 'p2.p_place AS place2', 338 | 'p3.p_place AS place3', 339 | 'p4.p_place AS place4', 340 | 'p5.p_place AS place5', 341 | 'p6.p_place AS place6', 342 | 'p7.p_place AS place7', 343 | 'p8.p_place AS place8', 344 | ]); 345 | 346 | // Filter each level of the hierarchy. 347 | foreach (explode(',', $search, 9) as $level => $string) { 348 | $prefix = ''; 349 | if ($startsWith) { 350 | $prefix = '%'; 351 | } 352 | $query->where('p' . $level . '.p_place', 'LIKE', $prefix . addcslashes(trim($string), '\\%_') . '%'); 353 | } 354 | 355 | $row_mapper = static function (stdClass $row) use ($tree): Place { 356 | $place = implode(', ', array_filter((array) $row)); 357 | 358 | return new Place($place, $tree); 359 | }; 360 | 361 | $filter = static function (): bool { 362 | return true; 363 | }; 364 | 365 | return $this->paginateQuery($query, $row_mapper, $filter, $offset, $limit); 366 | } 367 | 368 | } 369 | -------------------------------------------------------------------------------- /SharedPlacesModuleTrait.php: -------------------------------------------------------------------------------- 1 | '.CommonI18N::readme().'
    '; 34 | $link2 = ''.CommonI18N::readmeLocationData().''; 35 | 36 | $description = array(); 37 | //TODO add link to https://genealogy.net/GEDCOM/ 38 | $description[] = /* I18N: Module Configuration */I18N::translate('A module supporting shared places as level 0 GEDCOM objects, on the basis of the GEDCOM-L Addendum to the GEDCOM 5.5.1 specification. Shared places may contain e.g. map coordinates, notes and media objects. The module displays this data for all matching places via the extended \'Facts and events\' tab. It may also be used to manage GOV ids, in combination with the Gov4Webtrees module.'); 39 | /*$description[] =*/ /* I18N: Module Configuration *//*I18N::translate('Replaces the original \'Locations\' module.');*/ 40 | $description[] = 41 | CommonI18N::requires2(CommonI18N::titleVestaCommon(), CommonI18N::titleVestaPersonalFacts()); 42 | $description[] = 43 | CommonI18N::providesLocationData(); 44 | $description[] = $link1 . '. ' . $link2 . '.';; 45 | return $description; 46 | } 47 | 48 | protected function createPrefs() { 49 | $generalSub = array(); 50 | $generalSub[] = new ControlPanelSubsection( 51 | CommonI18N::displayedTitle(), 52 | array( 53 | /*new ControlPanelCheckbox( 54 | I18N::translate('Include the %1$s symbol in the module title', $this->getVestaSymbol()), 55 | null, 56 | 'VESTA', 57 | '1'),*/ 58 | new ControlPanelCheckbox( 59 | CommonI18N::vestaSymbolInListTitle(), 60 | CommonI18N::vestaSymbolInTitle2(), 61 | 'VESTA_LIST', 62 | '1'))); 63 | 64 | $link = ''.CommonI18N::readme().''; 65 | 66 | $generalSub[] = new ControlPanelSubsection( 67 | /* I18N: Module Configuration */I18N::translate('Shared place structure'), 68 | array(new ControlPanelCheckbox( 69 | /* I18N: Module Configuration */I18N::translate('Use hierarchical shared places'), 70 | /* I18N: Module Configuration */I18N::translate('If checked, relations between shared places are modelled via an explicit hierarchy, where shared places have XREFs to higher-level shared places, as described in the specification.') . ' ' . 71 | /* I18N: Module Configuration */I18N::translate('Note that this also affects the way shared places are created, and the way they are mapped to places.') . ' ' . 72 | /* I18N: Module Configuration */I18N::translate('In particular, hierarchical shared places do not have names with comma-separated name parts.') . ' ' . 73 | /* I18N: Module Configuration */I18N::translate('See %1$s for details.', $link) . ' ' . 74 | /* I18N: Module Configuration */I18N::translate('There is a data fix available which may be used to convert existing shared places.') . ' ' . 75 | /* I18N: Module Configuration */I18N::translate('When unchecked, the former approach is used, in which hierarchies are only hinted at by using shared place names with comma-separated name parts.') . ' ' . 76 | /* I18N: Module Configuration */I18N::translate('It is strongly recommended to switch to hierarchical shared places.'), 77 | 'USE_HIERARCHY', 78 | '1'))); 79 | 80 | $generalSub[] = new ControlPanelSubsection( 81 | /* I18N: Module Configuration */I18N::translate('Linking of shared places to places'), 82 | array( 83 | new ControlPanelCheckbox( 84 | /* I18N: Module Configuration */I18N::translate('Additionally link shared places via name'), 85 | /* I18N: Module Configuration */I18N::translate('According to the GEDCOM-L Addendum, shared places are referenced via XREFs, just like shared notes etc. ') . 86 | /* I18N: Module Configuration */I18N::translate('It is now recommended to use XREFs, as this improves performance and flexibility. There is a data fix available which may be used to add XREFs. ') . 87 | /* I18N: Module Configuration */I18N::translate('However, you can still check this option and link shared places via the place name itself. In this case, links are established internally by searching for a shared place with any name matching case-insensitively.') . ' ' . 88 | /* I18N: Module Configuration */I18N::translate('If you are using hierarchical shared places, a place with the name "A, B, C" is mapped to a shared place "A" with a higher-level shared place that maps to "B, C".'), 89 | 'INDIRECT_LINKS', 90 | '0'), 91 | new ControlPanelRange( 92 | /* I18N: Module Configuration */I18N::translate('... and fall back to n parent levels'), 93 | /* I18N: Module Configuration */I18N::translate('When the preceding option is checked, and no matching shared place is found, fall back to n parent places (so that e.g. for n=2 a place "A, B, C" would also match shared places that match "B, C" and "C")'), 94 | 0, 95 | 5, 96 | 'INDIRECT_LINKS_PARENT_LEVELS', 97 | 0))); 98 | 99 | $factsSub = array(); 100 | 101 | //TODO: make this configurable again? 102 | if (false) { 103 | $factsSub[] = new ControlPanelSubsection( 104 | /* I18N: Module Configuration */I18N::translate('All shared place facts'), 105 | array( 106 | ControlPanelFactRestriction::createWithFacts( 107 | SharedPlacesModuleTrait::getPicklistFactsLoc(), 108 | /* I18N: Module Configuration */I18N::translate('This is the list of GEDCOM facts that your users can add to shared places. You can modify this list by removing or adding fact names as necessary. Fact names that appear in this list must not also appear in the “Unique shared place facts” list.'), 109 | '_LOC_FACTS_ADD', 110 | 'NAME,_LOC:TYPE,NOTE,SHARED_NOTE,SOUR,_LOC:_LOC'))); 111 | $factsSub[] = new ControlPanelSubsection( 112 | /* I18N: Module Configuration */I18N::translate('Unique shared place facts'), 113 | array( 114 | ControlPanelFactRestriction::createWithFacts( 115 | SharedPlacesModuleTrait::getPicklistFactsLoc(), 116 | /* I18N: Module Configuration */I18N::translate('This is the list of GEDCOM facts that your users can only add once to shared places. For example, if NAME is in this list, users will not be able to add more than one NAME record to a shared place. Fact names that appear in this list must not also appear in the “All shared place facts” list.'), 117 | '_LOC_FACTS_UNIQUE', 118 | 'MAP,_GOV'))); 119 | 120 | //this is prepared for in the modal, but apparently was never used 121 | //really not that useful currently 122 | /* 123 | $factsSub[] = new ControlPanelSubsection( 124 | I18N::translate('Facts for new shared places'), 125 | array( 126 | ControlPanelFactRestriction::createWithFacts( 127 | SharedPlacesModuleTrait::getPicklistFactsLoc(true), 128 | I18N::translate('This is the list of GEDCOM facts that will be shown when adding a new shared place.'), 129 | '_LOC_FACTS_REQUIRED', 130 | ''))); 131 | */ 132 | 133 | $factsSub[] = new ControlPanelSubsection( 134 | /* I18N: Module Configuration */I18N::translate('Quick shared place facts'), 135 | array( 136 | ControlPanelFactRestriction::createWithFacts( 137 | SharedPlacesModuleTrait::getPicklistFactsLoc(), 138 | /* I18N: Module Configuration */I18N::translate('This is the list of GEDCOM facts that your users can add to shared places. You can modify this list by removing or adding fact names as necessary. Fact names that appear in this list must not also appear in the “Unique shared place facts” list. '), 139 | '_LOC_FACTS_QUICK', 140 | 'NAME,_LOC:_LOC,MAP,NOTE,SHARED_NOTE,_GOV'))); 141 | } 142 | 143 | $factsAndEventsSub = array(); 144 | $factsAndEventsSub[] = new ControlPanelSubsection( 145 | CommonI18N::displayedData(), 146 | array( 147 | new ControlPanelCheckbox( 148 | /* I18N: Module Configuration */I18N::translate('Restrict to specific facts and events'), 149 | /* I18N: Module Configuration */I18N::translate('If this option is checked, shared place data is only displayed for the following facts and events. ') . 150 | CommonI18N::bothEmpty(), 151 | 'RESTRICTED', 152 | '0'), 153 | ControlPanelFactRestriction::createWithIndividualFacts( 154 | CommonI18N::restrictIndi(), 155 | 'RESTRICTED_INDI', 156 | 'BIRT,OCCU,RESI,DEAT'), 157 | ControlPanelFactRestriction::createWithFamilyFacts( 158 | CommonI18N::restrictFam(), 159 | 'RESTRICTED_FAM', 160 | 'MARR'))); 161 | 162 | $factsAndEventsSub[] = new ControlPanelSubsection( 163 | /* I18N: Module Configuration */I18N::translate('Automatically expand shared place data'), 164 | array(new ControlPanelRadioButtons( 165 | false, 166 | array( 167 | new ControlPanelRadioButton( 168 | ' './* I18N: Module Configuration */MoreI18N::xlate('no'), 169 | null, 170 | '0'), 171 | new ControlPanelRadioButton( 172 | /* I18N: Module Configuration */I18N::translate('yes, but only the first occurrence of the shared place'), 173 | /* I18N: Module Configuration */I18N::translate('Note that the first occurrence may be within a toggleable, currently hidden fact or event (such as an event of a close relative). This will probably be improved in future versions of the module.'), 174 | '1'), 175 | new ControlPanelRadioButton( 176 | /* I18N: Module Configuration */MoreI18N::xlate('yes'), 177 | null, 178 | '2')), 179 | null, 180 | 'EXPAND', 181 | '1'))); 182 | 183 | $listSub = array(); 184 | $listSub[] = new ControlPanelSubsection( 185 | CommonI18N::displayedData(), 186 | array(new ControlPanelCheckbox( 187 | /* I18N: Module Configuration */I18N::translate('Show link counts for shared places list'), 188 | /* I18N: Module Configuration */I18N::translate('Determining the link counts (linked individual/families) is expensive when assigning shared places via name, and therefore causes delays when the shared places list is displayed. It\'s recommended to only select this option if places are assigned via XREFs.'), 189 | 'LINK_COUNTS', 190 | '1'))); 191 | 192 | $pageSub = array(); 193 | $pageSub[] = new ControlPanelSubsection( 194 | /* I18N: Module Configuration */I18N::translate('Summary'), 195 | array(new ControlPanelTextbox( 196 | /* I18N: Module Configuration */I18N::translate('Reference year'), 197 | /* I18N: Module Configuration */I18N::translate('The year set here may be used by other modules to enhance the place description with additional data. If left empty, the current year is used.'), 198 | 'REF_YEAR', 199 | '', 200 | false, 201 | 4, 202 | '[1-2][0-9][0-9][0-9]'))); 203 | 204 | $pageSub[] = new ControlPanelSubsection( 205 | CommonI18N::placeHistory(), 206 | array( 207 | new ControlPanelFactRestriction( 208 | PlaceHistory::getPicklistFacts(), 209 | CommonI18N::restrictPlaceHistory(), 210 | 'RESTRICTED_PLACE_HISTORY', 211 | PlaceHistory::initialFactsStringForPreferences()))); 212 | 213 | $hierarchySub = array(); 214 | $hierarchySub[] = new ControlPanelSubsection( 215 | CommonI18N::displayedData(), 216 | array(new ControlPanelCheckbox( 217 | /* I18N: Module Configuration */I18N::translate('Filter to unique shared places'), 218 | /* I18N: Module Configuration */I18N::translate('In the place hierarchy list, when using the option \'restrict to shared places\', shared places with multiple names show up multiple times as separate entries. Check this option to show each shared place only once in this case, under the shared place\'s primary name, and also show its additional names.'), 219 | 'UNIQUE_SP_IN_HIERARCHY', 220 | '0'))); 221 | 222 | $sections = array(); 223 | $sections[] = new ControlPanelSection( 224 | CommonI18N::general(), 225 | null, 226 | $generalSub); 227 | 228 | //TODO: support this again? 229 | if (false) { 230 | $sections[] = new ControlPanelSection( 231 | /* I18N: Module Configuration */I18N::translate('Facts for shared place records'), 232 | null, 233 | $factsSub); 234 | } 235 | 236 | $sections[] = new ControlPanelSection( 237 | CommonI18N::factsAndEventsTabSettings(), 238 | null, 239 | $factsAndEventsSub); 240 | $sections[] = new ControlPanelSection( 241 | /* I18N: Module Configuration */I18N::translate('Shared places list'), 242 | null, 243 | $listSub); 244 | $sections[] = new ControlPanelSection( 245 | /* I18N: Module Configuration */I18N::translate('Shared place page'), 246 | null, 247 | $pageSub); 248 | $sections[] = new ControlPanelSection( 249 | /* I18N: Module Configuration */MoreI18N::xlate('Place hierarchy'), 250 | null, 251 | $hierarchySub); 252 | 253 | return new ControlPanelPreferences($sections); 254 | } 255 | 256 | //TODO: this is webtrees_20! 257 | public static function getPicklistFactsLoc(bool $forRequired = false): array { 258 | $tags = [ 259 | "NAME", 260 | "_LOC:TYPE", 261 | "NOTE", 262 | "SHARED_NOTE", 263 | "SOUR", 264 | "_LOC:_LOC", 265 | "MAP", 266 | "_GOV"]; 267 | 268 | if ($forRequired) { 269 | //others are redundant, tricky, or anyway TBI 270 | $tags = [ 271 | "NOTE"]; 272 | 273 | //"SHARED_NOTE" problematic (potential modal within modal) 274 | } 275 | 276 | $facts = []; 277 | foreach ($tags as $tag) { 278 | $facts[$tag] = GedcomTag::getLabel($tag); 279 | } 280 | uasort($facts, '\Fisharebest\Webtrees\I18N::strcasecmp'); 281 | 282 | return $facts; 283 | } 284 | } 285 | --------------------------------------------------------------------------------