xref: /webtrees/app/Module/PlacesModule.php (revision 7413816e6dd2d50e569034fb804f3dce7471bb94)
11f374598SGreg Roach<?php
23976b470SGreg Roach
31f374598SGreg Roach/**
41f374598SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
61f374598SGreg Roach * This program is free software: you can redistribute it and/or modify
71f374598SGreg Roach * it under the terms of the GNU General Public License as published by
81f374598SGreg Roach * the Free Software Foundation, either version 3 of the License, or
91f374598SGreg Roach * (at your option) any later version.
101f374598SGreg Roach * This program is distributed in the hope that it will be useful,
111f374598SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
121f374598SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
131f374598SGreg Roach * GNU General Public License for more details.
141f374598SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
161f374598SGreg Roach */
17fcfa147eSGreg Roach
181f374598SGreg Roachdeclare(strict_types=1);
191f374598SGreg Roach
201f374598SGreg Roachnamespace Fisharebest\Webtrees\Module;
211f374598SGreg Roach
221f374598SGreg Roachuse Exception;
231f374598SGreg Roachuse Fisharebest\Webtrees\Fact;
248af6bbf8SGreg Roachuse Fisharebest\Webtrees\Family;
251f374598SGreg Roachuse Fisharebest\Webtrees\I18N;
261f374598SGreg Roachuse Fisharebest\Webtrees\Individual;
2709482a55SGreg Roachuse Fisharebest\Webtrees\Place;
285333da53SGreg Roachuse Fisharebest\Webtrees\PlaceLocation;
29c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\Services\LeafletJsService;
30c9c6f2ecSGreg Roachuse Fisharebest\Webtrees\Services\ModuleService;
3139ca88baSGreg Roachuse Illuminate\Support\Collection;
321f374598SGreg Roach
331f374598SGreg Roach/**
341f374598SGreg Roach * Class PlacesMapModule
351f374598SGreg Roach */
3637eb8894SGreg Roachclass PlacesModule extends AbstractModule implements ModuleTabInterface
371f374598SGreg Roach{
3849a243cbSGreg Roach    use ModuleTabTrait;
3949a243cbSGreg Roach
401191c60cSGreg Roach    protected const ICONS = [
414dbb8c13SGreg Roach        'FAM:CENS'  => ['color' => 'darkcyan', 'name' => 'list fas'],
42d0889c63SGreg Roach        'FAM:MARR'  => ['color' => 'green', 'name' => 'infinity fas'],
434dbb8c13SGreg Roach        'INDI:BAPM' => ['color' => 'hotpink', 'name' => 'water fas'],
444dbb8c13SGreg Roach        'INDI:BARM' => ['color' => 'hotpink', 'name' => 'star-of-david fas'],
454dbb8c13SGreg Roach        'INDI:BASM' => ['color' => 'hotpink', 'name' => 'star-of-david fas'],
464dbb8c13SGreg Roach        'INDI:BIRT' => ['color' => 'hotpink', 'name' => 'baby-carriage fas'],
47d0889c63SGreg Roach        'INDI:BURI' => ['color' => 'purple', 'name' => 'times fas'],
484dbb8c13SGreg Roach        'INDI:CENS' => ['color' => 'darkcyan', 'name' => 'list fas'],
494dbb8c13SGreg Roach        'INDI:CHR'  => ['color' => 'hotpink', 'name' => 'water fas'],
504dbb8c13SGreg Roach        'INDI:CHRA' => ['color' => 'hotpink', 'name' => 'water fas'],
51d0889c63SGreg Roach        'INDI:CREM' => ['color' => 'black', 'name' => 'times fas'],
52d0889c63SGreg Roach        'INDI:DEAT' => ['color' => 'black', 'name' => 'times fas'],
53d0889c63SGreg Roach        'INDI:EDUC' => ['color' => 'violet', 'name' => 'university fas'],
54d0889c63SGreg Roach        'INDI:GRAD' => ['color' => 'violet', 'name' => 'university fas'],
554dbb8c13SGreg Roach        'INDI:OCCU' => ['color' => 'darkcyan', 'name' => 'industry fas'],
564dbb8c13SGreg Roach        'INDI:RESI' => ['color' => 'darkcyan', 'name' => 'home fas'],
578af6bbf8SGreg Roach    ];
588af6bbf8SGreg Roach
59*5b8403dcSFranz Frese    protected const OWN_ICONS = [
60*5b8403dcSFranz Frese        'INDI:BIRT' => ['color' => 'red', 'name' => 'baby-carriage fas'],
61*5b8403dcSFranz Frese        'INDI:CHR'  => ['color' => 'red', 'name' => 'water fas'],
62*5b8403dcSFranz Frese    ] + self::ICONS;
63*5b8403dcSFranz Frese
6457862dd5SDavid Drury    protected const DEFAULT_ICON = ['color' => 'gold', 'name' => 'bullseye fas'];
658af6bbf8SGreg Roach
66c9c6f2ecSGreg Roach    private LeafletJsService $leaflet_js_service;
67c9c6f2ecSGreg Roach
68c9c6f2ecSGreg Roach    private ModuleService $module_service;
69c9c6f2ecSGreg Roach
70c9c6f2ecSGreg Roach    /**
71c9c6f2ecSGreg Roach     * @param LeafletJsService $leaflet_js_service
72c9c6f2ecSGreg Roach     * @param ModuleService    $module_service
73c9c6f2ecSGreg Roach     */
74c9c6f2ecSGreg Roach    public function __construct(LeafletJsService $leaflet_js_service, ModuleService $module_service)
75c9c6f2ecSGreg Roach    {
76c9c6f2ecSGreg Roach        $this->leaflet_js_service = $leaflet_js_service;
77c9c6f2ecSGreg Roach        $this->module_service = $module_service;
78c9c6f2ecSGreg Roach    }
79c9c6f2ecSGreg Roach
80961ec755SGreg Roach    /**
810cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
82961ec755SGreg Roach     *
83961ec755SGreg Roach     * @return string
84961ec755SGreg Roach     */
8549a243cbSGreg Roach    public function title(): string
861f374598SGreg Roach    {
87bbb76c12SGreg Roach        /* I18N: Name of a module */
88bbb76c12SGreg Roach        return I18N::translate('Places');
891f374598SGreg Roach    }
901f374598SGreg Roach
9149a243cbSGreg Roach    public function description(): string
921f374598SGreg Roach    {
93d21f0b10SGreg Roach        /* I18N: Description of the “Places” module */
94bbb76c12SGreg Roach        return I18N::translate('Show the location of events on a map.');
951f374598SGreg Roach    }
961f374598SGreg Roach
9749a243cbSGreg Roach    /**
9849a243cbSGreg Roach     * The default position for this tab.  It can be changed in the control panel.
9949a243cbSGreg Roach     *
10049a243cbSGreg Roach     * @return int
10149a243cbSGreg Roach     */
102cbf4b7faSGreg Roach    public function defaultTabOrder(): int
103cbf4b7faSGreg Roach    {
104fb7a0427SGreg Roach        return 8;
1051f374598SGreg Roach    }
1061f374598SGreg Roach
1073caaa4d2SGreg Roach    /**
1083caaa4d2SGreg Roach     * Is this tab empty? If so, we don't always need to display it.
1093caaa4d2SGreg Roach     *
1103caaa4d2SGreg Roach     * @param Individual $individual
1113caaa4d2SGreg Roach     *
1123caaa4d2SGreg Roach     * @return bool
1133caaa4d2SGreg Roach     */
1148f53f488SRico Sonntag    public function hasTabContent(Individual $individual): bool
1151f374598SGreg Roach    {
116c9c6f2ecSGreg Roach        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
1171f374598SGreg Roach
118c9c6f2ecSGreg Roach        return $map_providers->isNotEmpty() && $this->getMapData($individual)->features !== [];
1191f374598SGreg Roach    }
1201f374598SGreg Roach
1211f374598SGreg Roach    /**
122e93111adSRico Sonntag     * @param Individual $indi
1231f374598SGreg Roach     *
124ac701fbdSGreg Roach     * @return object
1251f374598SGreg Roach     */
126ac701fbdSGreg Roach    private function getMapData(Individual $indi): object
1271f374598SGreg Roach    {
12805ff554cSGreg Roach        $facts = $this->getPersonalFacts($indi);
1291f374598SGreg Roach
1301f374598SGreg Roach        $geojson = [
1311f374598SGreg Roach            'type'     => 'FeatureCollection',
1321f374598SGreg Roach            'features' => [],
1331f374598SGreg Roach        ];
13405ff554cSGreg Roach
1351f374598SGreg Roach        foreach ($facts as $id => $fact) {
1365333da53SGreg Roach            $location = new PlaceLocation($fact->place()->gedcomName());
1378af6bbf8SGreg Roach
1388af6bbf8SGreg Roach            // Use the co-ordinates from the fact (if they exist).
1398af6bbf8SGreg Roach            $latitude  = $fact->latitude();
1408af6bbf8SGreg Roach            $longitude = $fact->longitude();
1418af6bbf8SGreg Roach
1428af6bbf8SGreg Roach            // Use the co-ordinates from the location otherwise.
14390949315SGreg Roach            if ($latitude === null || $longitude === null) {
1448af6bbf8SGreg Roach                $latitude  = $location->latitude();
1458af6bbf8SGreg Roach                $longitude = $location->longitude();
1468af6bbf8SGreg Roach            }
1478af6bbf8SGreg Roach
148*5b8403dcSFranz Frese            $icons = $fact->record() === $indi ? static::OWN_ICONS : static::ICONS;
149624a1c2fSFranz Frese
15090949315SGreg Roach            if ($latitude !== null && $longitude !== null) {
1511f374598SGreg Roach                $geojson['features'][] = [
1521f374598SGreg Roach                    'type'       => 'Feature',
1531f374598SGreg Roach                    'id'         => $id,
1541f374598SGreg Roach                    'geometry'   => [
1551f374598SGreg Roach                        'type'        => 'Point',
156f465e1eaSGreg Roach                        'coordinates' => [$longitude, $latitude],
1571f374598SGreg Roach                    ],
1581f374598SGreg Roach                    'properties' => [
159*5b8403dcSFranz Frese                        'icon'    => $icons[$fact->tag()] ?? static::DEFAULT_ICON,
160497231cdSGreg Roach                        'tooltip' => $fact->place()->gedcomName(),
1618af6bbf8SGreg Roach                        'summary' => view('modules/places/event-sidebar', $this->summaryData($indi, $fact)),
1621f374598SGreg Roach                    ],
1631f374598SGreg Roach                ];
1641f374598SGreg Roach            }
1651f374598SGreg Roach        }
1661f374598SGreg Roach
16705ff554cSGreg Roach        return (object) $geojson;
1681f374598SGreg Roach    }
1691f374598SGreg Roach
1701f374598SGreg Roach    /**
17105ff554cSGreg Roach     * @param Individual $individual
172c8a5344dSGreg Roach     *
17336779af1SGreg Roach     * @return Collection<int,Fact>
174c8a5344dSGreg Roach     * @throws Exception
175c8a5344dSGreg Roach     */
176c8a5344dSGreg Roach    private function getPersonalFacts(Individual $individual): Collection
177c8a5344dSGreg Roach    {
178c8a5344dSGreg Roach        $facts = $individual->facts();
179c8a5344dSGreg Roach
180c8a5344dSGreg Roach        foreach ($individual->spouseFamilies() as $family) {
181c8a5344dSGreg Roach            $facts = $facts->merge($family->facts());
182c8a5344dSGreg Roach            // Add birth of children from this family to the facts array
183c8a5344dSGreg Roach            foreach ($family->children() as $child) {
184c8a5344dSGreg Roach                $childsBirth = $child->facts(['BIRT'])->first();
185c8a5344dSGreg Roach                if ($childsBirth instanceof Fact && $childsBirth->place()->gedcomName() !== '') {
186c8a5344dSGreg Roach                    $facts->push($childsBirth);
187c8a5344dSGreg Roach                }
188c8a5344dSGreg Roach            }
189c8a5344dSGreg Roach        }
190c8a5344dSGreg Roach
191c8a5344dSGreg Roach        $facts = Fact::sortFacts($facts);
192c8a5344dSGreg Roach
193624a1c2fSFranz Frese        return $facts->filter(static function (Fact $item): bool {
194624a1c2fSFranz Frese            return $item->place()->gedcomName() !== '';
195624a1c2fSFranz Frese        });
196c8a5344dSGreg Roach    }
197c8a5344dSGreg Roach
198c8a5344dSGreg Roach    /**
199c8a5344dSGreg Roach     * @param Individual $individual
2008af6bbf8SGreg Roach     * @param Fact       $fact
2018af6bbf8SGreg Roach     *
20209482a55SGreg Roach     * @return array<string|Place>
2038af6bbf8SGreg Roach     */
2048af6bbf8SGreg Roach    private function summaryData(Individual $individual, Fact $fact): array
2058af6bbf8SGreg Roach    {
2068af6bbf8SGreg Roach        $record = $fact->record();
2078af6bbf8SGreg Roach        $name   = '';
2088af6bbf8SGreg Roach        $url    = '';
2098af6bbf8SGreg Roach        $tag    = $fact->label();
2108af6bbf8SGreg Roach
2118af6bbf8SGreg Roach        if ($record instanceof Family) {
2128af6bbf8SGreg Roach            // Marriage
21339ca88baSGreg Roach            $spouse = $record->spouse($individual);
2148af6bbf8SGreg Roach            if ($spouse instanceof Individual) {
2158af6bbf8SGreg Roach                $url  = $spouse->url();
21639ca88baSGreg Roach                $name = $spouse->fullName();
2178af6bbf8SGreg Roach            }
2188af6bbf8SGreg Roach        } elseif ($record !== $individual) {
2198af6bbf8SGreg Roach            // Birth of a child
2208af6bbf8SGreg Roach            $url  = $record->url();
22139ca88baSGreg Roach            $name = $record->fullName();
22225cac958SGreg Roach            $tag  = I18N::translate('Birth of a child');
2238af6bbf8SGreg Roach        }
2248af6bbf8SGreg Roach
2258af6bbf8SGreg Roach        return [
2268af6bbf8SGreg Roach            'tag'   => $tag,
2278af6bbf8SGreg Roach            'url'   => $url,
2288af6bbf8SGreg Roach            'name'  => $name,
2298af6bbf8SGreg Roach            'value' => $fact->value(),
23066ecd017SGreg Roach            'date'  => $fact->date()->display($individual->tree(), null, true),
2318af6bbf8SGreg Roach            'place' => $fact->place(),
2328af6bbf8SGreg Roach        ];
2338af6bbf8SGreg Roach    }
234c9c6f2ecSGreg Roach
235c9c6f2ecSGreg Roach    /**
236c9c6f2ecSGreg Roach     * A greyed out tab has no actual content, but may perhaps have
237c9c6f2ecSGreg Roach     * options to create content.
238c9c6f2ecSGreg Roach     *
239c9c6f2ecSGreg Roach     * @param Individual $individual
240c9c6f2ecSGreg Roach     *
241c9c6f2ecSGreg Roach     * @return bool
242c9c6f2ecSGreg Roach     */
243c9c6f2ecSGreg Roach    public function isGrayedOut(Individual $individual): bool
244c9c6f2ecSGreg Roach    {
245c9c6f2ecSGreg Roach        return false;
246c9c6f2ecSGreg Roach    }
247c9c6f2ecSGreg Roach
248c9c6f2ecSGreg Roach    /**
249c9c6f2ecSGreg Roach     * Can this tab load asynchronously?
250c9c6f2ecSGreg Roach     *
251c9c6f2ecSGreg Roach     * @return bool
252c9c6f2ecSGreg Roach     */
253c9c6f2ecSGreg Roach    public function canLoadAjax(): bool
254c9c6f2ecSGreg Roach    {
255c9c6f2ecSGreg Roach        return true;
256c9c6f2ecSGreg Roach    }
257c9c6f2ecSGreg Roach
258c9c6f2ecSGreg Roach    /**
259c9c6f2ecSGreg Roach     * Generate the HTML content of this tab.
260c9c6f2ecSGreg Roach     *
261c9c6f2ecSGreg Roach     * @param Individual $individual
262c9c6f2ecSGreg Roach     *
263c9c6f2ecSGreg Roach     * @return string
264c9c6f2ecSGreg Roach     */
265c9c6f2ecSGreg Roach    public function getTabContent(Individual $individual): string
266c9c6f2ecSGreg Roach    {
267c9c6f2ecSGreg Roach        return view('modules/places/tab', [
268c9c6f2ecSGreg Roach            'data'           => $this->getMapData($individual),
269c9c6f2ecSGreg Roach            'leaflet_config' => $this->leaflet_js_service->config(),
270c9c6f2ecSGreg Roach        ]);
271c9c6f2ecSGreg Roach    }
2721f374598SGreg Roach}
273