PlacesModule   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 86
dl 0
loc 230
rs 10
c 1
b 0
f 0
wmc 24

11 Methods

Rating   Name   Duplication   Size   Complexity  
A description() 0 4 1
A __construct() 0 4 1
B getMapData() 0 40 6
A hasTabContent() 0 5 2
A defaultTabOrder() 0 3 1
A title() 0 4 1
A canLoadAjax() 0 3 1
A isGrayedOut() 0 3 1
A summaryData() 0 28 4
A getPersonalFacts() 0 18 5
A getTabContent() 0 5 1
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Exception;
23
use Fisharebest\Webtrees\Fact;
24
use Fisharebest\Webtrees\Family;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Place;
28
use Fisharebest\Webtrees\PlaceLocation;
29
use Fisharebest\Webtrees\Services\LeafletJsService;
30
use Fisharebest\Webtrees\Services\ModuleService;
31
use Illuminate\Support\Collection;
32
33
/**
34
 * Class PlacesMapModule
35
 */
36
class PlacesModule extends AbstractModule implements ModuleTabInterface
37
{
38
    use ModuleTabTrait;
39
40
    protected const ICONS = [
41
        'FAM:CENS'  => ['color' => 'darkcyan', 'name' => 'list fas'],
42
        'FAM:MARR'  => ['color' => 'green', 'name' => 'infinity fas'],
43
        'INDI:BAPM' => ['color' => 'hotpink', 'name' => 'water fas'],
44
        'INDI:BARM' => ['color' => 'hotpink', 'name' => 'star-of-david fas'],
45
        'INDI:BASM' => ['color' => 'hotpink', 'name' => 'star-of-david fas'],
46
        'INDI:BIRT' => ['color' => 'hotpink', 'name' => 'baby-carriage fas'],
47
        'INDI:BURI' => ['color' => 'purple', 'name' => 'times fas'],
48
        'INDI:CENS' => ['color' => 'darkcyan', 'name' => 'list fas'],
49
        'INDI:CHR'  => ['color' => 'hotpink', 'name' => 'water fas'],
50
        'INDI:CHRA' => ['color' => 'hotpink', 'name' => 'water fas'],
51
        'INDI:CREM' => ['color' => 'black', 'name' => 'times fas'],
52
        'INDI:DEAT' => ['color' => 'black', 'name' => 'times fas'],
53
        'INDI:EDUC' => ['color' => 'violet', 'name' => 'university fas'],
54
        'INDI:GRAD' => ['color' => 'violet', 'name' => 'university fas'],
55
        'INDI:OCCU' => ['color' => 'darkcyan', 'name' => 'industry fas'],
56
        'INDI:RESI' => ['color' => 'darkcyan', 'name' => 'home fas'],
57
    ];
58
59
    protected const DEFAULT_ICON = ['color' => 'gold', 'name' => 'bullseye fas'];
60
61
    private LeafletJsService $leaflet_js_service;
62
63
    private ModuleService $module_service;
64
65
    /**
66
     * @param LeafletJsService $leaflet_js_service
67
     * @param ModuleService    $module_service
68
     */
69
    public function __construct(LeafletJsService $leaflet_js_service, ModuleService $module_service)
70
    {
71
        $this->leaflet_js_service = $leaflet_js_service;
72
        $this->module_service = $module_service;
73
    }
74
75
    /**
76
     * How should this module be identified in the control panel, etc.?
77
     *
78
     * @return string
79
     */
80
    public function title(): string
81
    {
82
        /* I18N: Name of a module */
83
        return I18N::translate('Places');
84
    }
85
86
    /**
87
     * A sentence describing what this module does.
88
     *
89
     * @return string
90
     */
91
    public function description(): string
92
    {
93
        /* I18N: Description of the “Places” module */
94
        return I18N::translate('Show the location of events on a map.');
95
    }
96
97
    /**
98
     * The default position for this tab.  It can be changed in the control panel.
99
     *
100
     * @return int
101
     */
102
    public function defaultTabOrder(): int
103
    {
104
        return 8;
105
    }
106
107
    /**
108
     * Is this tab empty? If so, we don't always need to display it.
109
     *
110
     * @param Individual $individual
111
     *
112
     * @return bool
113
     */
114
    public function hasTabContent(Individual $individual): bool
115
    {
116
        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
117
118
        return $map_providers->isNotEmpty() && $this->getMapData($individual)->features !== [];
119
    }
120
121
    /**
122
     * @param Individual $indi
123
     *
124
     * @return object
125
     */
126
    private function getMapData(Individual $indi): object
127
    {
128
        $facts = $this->getPersonalFacts($indi);
129
130
        $geojson = [
131
            'type'     => 'FeatureCollection',
132
            'features' => [],
133
        ];
134
135
        foreach ($facts as $id => $fact) {
136
            $location = new PlaceLocation($fact->place()->gedcomName());
137
138
            // Use the co-ordinates from the fact (if they exist).
139
            $latitude  = $fact->latitude();
140
            $longitude = $fact->longitude();
141
142
            // Use the co-ordinates from the location otherwise.
143
            if ($latitude === null || $longitude === null) {
144
                $latitude  = $location->latitude();
145
                $longitude = $location->longitude();
146
            }
147
148
            if ($latitude !== null && $longitude !== null) {
149
                $geojson['features'][] = [
150
                    'type'       => 'Feature',
151
                    'id'         => $id,
152
                    'geometry'   => [
153
                        'type'        => 'Point',
154
                        'coordinates' => [$longitude, $latitude],
155
                    ],
156
                    'properties' => [
157
                        'icon'    => static::ICONS[$fact->tag()] ?? static::DEFAULT_ICON,
158
                        'tooltip' => $fact->place()->gedcomName(),
159
                        'summary' => view('modules/places/event-sidebar', $this->summaryData($indi, $fact)),
160
                    ],
161
                ];
162
            }
163
        }
164
165
        return (object) $geojson;
166
    }
167
168
    /**
169
     * @param Individual $individual
170
     *
171
     * @return Collection<int,Fact>
172
     * @throws Exception
173
     */
174
    private function getPersonalFacts(Individual $individual): Collection
175
    {
176
        $facts = $individual->facts();
177
178
        foreach ($individual->spouseFamilies() as $family) {
179
            $facts = $facts->merge($family->facts());
180
            // Add birth of children from this family to the facts array
181
            foreach ($family->children() as $child) {
182
                $childsBirth = $child->facts(['BIRT'])->first();
183
                if ($childsBirth instanceof Fact && $childsBirth->place()->gedcomName() !== '') {
184
                    $facts->push($childsBirth);
185
                }
186
            }
187
        }
188
189
        $facts = Fact::sortFacts($facts);
190
191
        return $facts->filter(static fn (Fact $item): bool => $item->place()->gedcomName() !== '');
192
    }
193
194
    /**
195
     * @param Individual $individual
196
     * @param Fact       $fact
197
     *
198
     * @return array<string|Place>
199
     */
200
    private function summaryData(Individual $individual, Fact $fact): array
201
    {
202
        $record = $fact->record();
203
        $name   = '';
204
        $url    = '';
205
        $tag    = $fact->label();
206
207
        if ($record instanceof Family) {
208
            // Marriage
209
            $spouse = $record->spouse($individual);
210
            if ($spouse instanceof Individual) {
211
                $url  = $spouse->url();
212
                $name = $spouse->fullName();
213
            }
214
        } elseif ($record !== $individual) {
215
            // Birth of a child
216
            $url  = $record->url();
217
            $name = $record->fullName();
218
            $tag  = I18N::translate('Birth of a child');
219
        }
220
221
        return [
222
            'tag'   => $tag,
223
            'url'   => $url,
224
            'name'  => $name,
225
            'value' => $fact->value(),
226
            'date'  => $fact->date()->display($individual->tree(), null, true),
227
            'place' => $fact->place(),
228
        ];
229
    }
230
231
    /**
232
     * A greyed out tab has no actual content, but may perhaps have
233
     * options to create content.
234
     *
235
     * @param Individual $individual
236
     *
237
     * @return bool
238
     */
239
    public function isGrayedOut(Individual $individual): bool
240
    {
241
        return false;
242
    }
243
244
    /**
245
     * Can this tab load asynchronously?
246
     *
247
     * @return bool
248
     */
249
    public function canLoadAjax(): bool
250
    {
251
        return true;
252
    }
253
254
    /**
255
     * Generate the HTML content of this tab.
256
     *
257
     * @param Individual $individual
258
     *
259
     * @return string
260
     */
261
    public function getTabContent(Individual $individual): string
262
    {
263
        return view('modules/places/tab', [
264
            'data'           => $this->getMapData($individual),
265
            'leaflet_config' => $this->leaflet_js_service->config(),
266
        ]);
267
    }
268
}
269