Completed
Push — develop ( 4b9394...9b940a )
by Greg
10:11
created

PlacesModule::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 4
rs 10
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 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\PlaceLocation;
28
use Fisharebest\Webtrees\Services\LeafletJsService;
29
use Fisharebest\Webtrees\Services\ModuleService;
30
use Illuminate\Support\Collection;
31
use stdClass;
32
33
/**
34
 * Class PlacesMapModule
35
 */
36
class PlacesModule extends AbstractModule implements ModuleTabInterface
37
{
38
    use ModuleTabTrait;
39
40
    protected const ICONS = [
41
        'BIRT' => ['color' => 'pink', 'name' => 'baby-carriage fas'],
42
        'BAPM' => ['color' => 'pink', 'name' => 'water fas'],
43
        'BARM' => ['color' => 'pink', 'name' => 'star-of-david fas'],
44
        'BASM' => ['color' => 'pink', 'name' => 'star-of-david fas'],
45
        'CHR'  => ['color' => 'pink', 'name' => 'water fas'],
46
        'CHRA' => ['color' => 'pink', 'name' => 'water fas'],
47
        'MARR' => ['color' => 'green', 'name' => 'infinity fas'],
48
        'DEAT' => ['color' => 'black', 'name' => 'times fas'],
49
        'BURI' => ['color' => 'purple', 'name' => 'times fas'],
50
        'CREM' => ['color' => 'black', 'name' => 'times fas'],
51
        'CENS' => ['color' => 'cyan', 'name' => 'list fas'],
52
        'RESI' => ['color' => 'cyan', 'name' => 'home fas'],
53
        'OCCU' => ['color' => 'cyan', 'name' => 'industry fas'],
54
        'GRAD' => ['color' => 'violet', 'name' => 'university fas'],
55
        'EDUC' => ['color' => 'violet', 'name' => 'university fas'],
56
    ];
57
58
    protected const DEFAULT_ICON = ['color' => 'gold', 'name' => 'bullseye fas'];
59
60
    private LeafletJsService $leaflet_js_service;
61
62
    private ModuleService $module_service;
63
64
    /**
65
     * PlacesModule constructor.
66
     *
67
     * @param LeafletJsService $leaflet_js_service
68
     * @param ModuleService    $module_service
69
     */
70
    public function __construct(LeafletJsService $leaflet_js_service, ModuleService $module_service)
71
    {
72
        $this->leaflet_js_service = $leaflet_js_service;
73
        $this->module_service = $module_service;
74
    }
75
76
    /**
77
     * How should this module be identified in the control panel, etc.?
78
     *
79
     * @return string
80
     */
81
    public function title(): string
82
    {
83
        /* I18N: Name of a module */
84
        return I18N::translate('Places');
85
    }
86
87
    /**
88
     * A sentence describing what this module does.
89
     *
90
     * @return string
91
     */
92
    public function description(): string
93
    {
94
        /* I18N: Description of the “Places” module */
95
        return I18N::translate('Show the location of events on a map.');
96
    }
97
98
    /**
99
     * The default position for this tab.  It can be changed in the control panel.
100
     *
101
     * @return int
102
     */
103
    public function defaultTabOrder(): int
104
    {
105
        return 8;
106
    }
107
108
    /**
109
     * Is this tab empty? If so, we don't always need to display it.
110
     *
111
     * @param Individual $individual
112
     *
113
     * @return bool
114
     */
115
    public function hasTabContent(Individual $individual): bool
116
    {
117
        $map_providers = $this->module_service->findByInterface(ModuleMapProviderInterface::class);
118
119
        return $map_providers->isNotEmpty() && $this->getMapData($individual)->features !== [];
120
    }
121
122
    /**
123
     * @param Individual $indi
124
     *
125
     * @return stdClass
126
     */
127
    private function getMapData(Individual $indi): stdClass
128
    {
129
        $facts = $this->getPersonalFacts($indi);
130
131
        $geojson = [
132
            'type'     => 'FeatureCollection',
133
            'features' => [],
134
        ];
135
136
        foreach ($facts as $id => $fact) {
137
            $location = new PlaceLocation($fact->place()->gedcomName());
138
139
            // Use the co-ordinates from the fact (if they exist).
140
            $latitude  = $fact->latitude();
141
            $longitude = $fact->longitude();
142
143
            // Use the co-ordinates from the location otherwise.
144
            if ($latitude === null || $longitude === null) {
145
                $latitude  = $location->latitude();
146
                $longitude = $location->longitude();
147
            }
148
149
            if ($latitude !== null && $longitude !== null) {
150
                $geojson['features'][] = [
151
                    'type'       => 'Feature',
152
                    'id'         => $id,
153
                    'geometry'   => [
154
                        'type'        => 'Point',
155
                        'coordinates' => [$longitude, $latitude],
156
                    ],
157
                    'properties' => [
158
                        'icon'    => static::ICONS[$fact->getTag()] ?? static::DEFAULT_ICON,
159
                        'tooltip' => $fact->place()->gedcomName(),
160
                        'summary' => view('modules/places/event-sidebar', $this->summaryData($indi, $fact)),
161
                    ],
162
                ];
163
            }
164
        }
165
166
        return (object) $geojson;
167
    }
168
169
    /**
170
     * @param Individual $individual
171
     *
172
     * @return Collection<Fact>
173
     * @throws Exception
174
     */
175
    private function getPersonalFacts(Individual $individual): Collection
176
    {
177
        $facts = $individual->facts();
178
179
        foreach ($individual->spouseFamilies() as $family) {
180
            $facts = $facts->merge($family->facts());
181
            // Add birth of children from this family to the facts array
182
            foreach ($family->children() as $child) {
183
                $childsBirth = $child->facts(['BIRT'])->first();
184
                if ($childsBirth instanceof Fact && $childsBirth->place()->gedcomName() !== '') {
185
                    $facts->push($childsBirth);
186
                }
187
            }
188
        }
189
190
        $facts = Fact::sortFacts($facts);
191
192
        return $facts->filter(static function (Fact $item): bool {
193
            return $item->place()->gedcomName() !== '';
194
        });
195
    }
196
197
    /**
198
     * @param Individual $individual
199
     * @param Fact       $fact
200
     *
201
     * @return mixed[]
202
     */
203
    private function summaryData(Individual $individual, Fact $fact): array
204
    {
205
        $record = $fact->record();
206
        $name   = '';
207
        $url    = '';
208
        $tag    = $fact->label();
209
210
        if ($record instanceof Family) {
211
            // Marriage
212
            $spouse = $record->spouse($individual);
213
            if ($spouse instanceof Individual) {
214
                $url  = $spouse->url();
215
                $name = $spouse->fullName();
216
            }
217
        } elseif ($record !== $individual) {
1 ignored issue
show
introduced by
The condition $record !== $individual is always true.
Loading history...
218
            // Birth of a child
219
            $url  = $record->url();
220
            $name = $record->fullName();
221
            $tag  = I18N::translate('Birth of a child');
222
        }
223
224
        return [
225
            'tag'   => $tag,
226
            'url'   => $url,
227
            'name'  => $name,
228
            'value' => $fact->value(),
229
            'date'  => $fact->date()->display(true),
230
            'place' => $fact->place(),
231
        ];
232
    }
233
234
    /**
235
     * A greyed out tab has no actual content, but may perhaps have
236
     * options to create content.
237
     *
238
     * @param Individual $individual
239
     *
240
     * @return bool
241
     */
242
    public function isGrayedOut(Individual $individual): bool
243
    {
244
        return false;
245
    }
246
247
    /**
248
     * Can this tab load asynchronously?
249
     *
250
     * @return bool
251
     */
252
    public function canLoadAjax(): bool
253
    {
254
        return true;
255
    }
256
257
    /**
258
     * Generate the HTML content of this tab.
259
     *
260
     * @param Individual $individual
261
     *
262
     * @return string
263
     */
264
    public function getTabContent(Individual $individual): string
265
    {
266
        return view('modules/places/tab', [
267
            'data'           => $this->getMapData($individual),
268
            'leaflet_config' => $this->leaflet_js_service->config(),
269
        ]);
270
    }
271
}
272