Passed
Branch feature/2.1-geodispersion-dev (1d61a8)
by Jonathan
61:21
created

GeoAnalysisViewDataService   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 199
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 77
c 1
b 0
f 0
dl 0
loc 199
rs 10
wmc 26

9 Methods

Rating   Name   Duplication   Size   Complexity  
A mapperConfigDecoder() 0 16 6
A colorsDecoder() 0 15 4
A mapAdapters() 0 8 1
A enabledFilter() 0 4 2
A all() 0 12 1
A viewMapper() 0 26 6
A mapAdapterMapper() 0 20 4
A __construct() 0 3 1
A find() 0 3 1
1
<?php
2
3
/**
4
 * webtrees-lib: MyArtJaub library for webtrees
5
 *
6
 * @package MyArtJaub\Webtrees
7
 * @subpackage GeoDispersion
8
 * @author Jonathan Jaubart <[email protected]>
9
 * @copyright Copyright (c) 2021, Jonathan Jaubart
10
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License, version 3
11
 */
12
13
declare(strict_types=1);
14
15
namespace MyArtJaub\Webtrees\Module\GeoDispersion\Services;
16
17
use Fisharebest\Webtrees\Registry;
18
use Fisharebest\Webtrees\Tree;
19
use Illuminate\Contracts\Container\BindingResolutionException;
20
use Illuminate\Database\Capsule\Manager as DB;
21
use Illuminate\Support\Collection;
22
use MyArtJaub\Webtrees\Common\GeoDispersion\Config\MapColorsConfig;
23
use MyArtJaub\Webtrees\Common\GeoDispersion\Config\MapViewConfig;
24
use MyArtJaub\Webtrees\Contracts\GeoDispersion\GeoAnalysisInterface;
25
use MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperConfigInterface;
26
use MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperInterface;
27
use MyArtJaub\Webtrees\Module\GeoDispersion\Model\GeoAnalysisMapAdapter;
28
use MyArtJaub\Webtrees\Module\GeoDispersion\Views\AbstractGeoAnalysisView;
29
use MyArtJaub\Webtrees\Module\GeoDispersion\Views\GeoAnalysisMap;
30
use Spatie\Color\Exceptions\InvalidColorValue;
31
use Closure;
32
use stdClass;
33
34
/**
35
 * Service for accessing Geographical dispersion analysis views configuration data.
36
 */
37
class GeoAnalysisViewDataService
38
{
39
    private MapDefinitionsService $mapdefinition_service;
40
41
    /**
42
     * Constructor for GeoAnalysisViewDataService
43
     *
44
     * @param MapDefinitionsService $mapdefinition_service
45
     */
46
    public function __construct(MapDefinitionsService $mapdefinition_service)
47
    {
48
        $this->mapdefinition_service = $mapdefinition_service;
49
    }
50
51
    /**
52
     * Find a Geographical dispersion analysis view by ID
53
     *
54
     * @param Tree $tree
55
     * @param int $id
56
     * @return AbstractGeoAnalysisView|NULL
57
     */
58
    public function find(Tree $tree, int $id): ?AbstractGeoAnalysisView
59
    {
60
        return $this->all($tree)->first(fn(AbstractGeoAnalysisView $view): bool => $view->id() === $id);
61
    }
62
63
    /**
64
     * Get all Geographical dispersion analysis views, with or without the disabled ones.
65
     *
66
     * {@internal It would ignore any view for which the class could not be loaded by the container}
67
     *
68
     * @param Tree $tree
69
     * @param bool $include_disabled
70
     * @return Collection<AbstractGeoAnalysisView>
71
     */
72
    public function all(Tree $tree, bool $include_disabled = false): Collection
73
    {
74
        return Registry::cache()->array()->remember(
75
            'all-geodispersion-views',
76
            function () use ($tree, $include_disabled): Collection {
77
                return DB::table('maj_geodisp_views')
78
                    ->select('maj_geodisp_views.*')
79
                    ->where('majgv_gedcom_id', '=', $tree->id())
80
                    ->get()
81
                    ->map($this->viewMapper($tree))
82
                    ->filter()
83
                    ->filter($this->enabledFilter($include_disabled));
84
            }
85
        );
86
    }
87
88
    /**
89
     * Get all GeoAnalysisMapAdapters linked to a Map View.
90
     *
91
     * @param GeoAnalysisMap $map_view
92
     * @return Collection<GeoAnalysisMapAdapter>
93
     */
94
    public function mapAdapters(GeoAnalysisMap $map_view): Collection
95
    {
96
        return DB::table('maj_geodisp_mapviews')
97
            ->select('maj_geodisp_mapviews.*')
98
            ->where('majgm_majgv_id', '=', $map_view->id())
99
            ->get()
100
            ->map($this->mapAdapterMapper())
101
            ->filter();
102
    }
103
104
    /**
105
     * Get the closure to create a AbstractGeoAnalysisView object from a row in the database.
106
     * It returns null if the classes stored in the DB cannot be loaded through the Laravel container,
107
     * or if the types do not match with the ones expected.
108
     *
109
     * @param Tree $tree
110
     * @return Closure(\stdClass $row):?AbstractGeoAnalysisView
111
     */
112
    private function viewMapper(Tree $tree): Closure
113
    {
114
        return function (stdClass $row) use ($tree): ?AbstractGeoAnalysisView {
115
            try {
116
                $geoanalysis = app($row->majgv_analysis);
117
                if (!($geoanalysis instanceof GeoAnalysisInterface)) {
118
                    return null;
119
                }
120
121
                $view = app()->makeWith($row->majgv_view_class, [
122
                    'id'                    =>  (int) $row->majgv_id,
123
                    'tree'                  =>  $tree,
124
                    'enabled'               =>  $row->majgv_status === 'enabled',
125
                    'description'           =>  $row->majgv_descr,
126
                    'geoanalysis'           =>  $geoanalysis,
127
                    'depth'                 =>  (int) $row->majgv_place_depth,
128
                    'detailed_top_places'   =>  (int) $row->majgv_top_places
129
                ]);
130
131
                if ($row->majgv_colors !== null && $view instanceof GeoAnalysisMap) {
132
                    $view->setColors($this->colorsDecoder($row->majgv_colors));
133
                }
134
135
                return $view instanceof AbstractGeoAnalysisView ? $view : null;
136
            } catch (BindingResolutionException $ex) {
137
                return null;
138
            }
139
        };
140
    }
141
142
    /**
143
     * Get the closure to create a GeoAnalysisMapAdapter object from a row in the database.
144
     * It returns null if the classes stored in the DB cannot be loaded through the Laravel container,
145
     * or if the types do not match with the ones expected.
146
     *
147
     * @return Closure(\stdClass $row):?GeoAnalysisMapAdapter
148
     */
149
    private function mapAdapterMapper(): Closure
150
    {
151
        return function (stdClass $row): ?GeoAnalysisMapAdapter {
152
            if (null === $map = $this->mapdefinition_service->find($row->majgm_map_id)) {
153
                return null;
154
            }
155
            try {
156
                $mapper = app($row->majgm_mapper);
157
                if (!($mapper instanceof PlaceMapperInterface)) {
158
                    return null;
159
                }
160
161
                return new GeoAnalysisMapAdapter(
162
                    (int) $row->majgm_id,
163
                    $map,
164
                    app($row->majgm_mapper),
0 ignored issues
show
Bug introduced by
It seems like app($row->majgm_mapper) can also be of type Illuminate\Container\Container; however, parameter $mapper of MyArtJaub\Webtrees\Modul...pAdapter::__construct() does only seem to accept MyArtJaub\Webtrees\Contr...on\PlaceMapperInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

164
                    /** @scrutinizer ignore-type */ app($row->majgm_mapper),
Loading history...
165
                    new MapViewConfig($row->majgm_feature_prop, $this->mapperConfigDecoder($row->majgm_config))
166
                );
167
            } catch (BindingResolutionException $ex) {
168
                return null;
169
            }
170
        };
171
    }
172
173
    /**
174
     * Create a PlaceMapperConfigInterface object from a JSON column value.
175
     * Returns null if the JSON string is invalid/empty or if the extracted mapper class cannot be loaded
176
     * through the Laravel container or if the type do not match with the one expected.
177
     *
178
     * @param string $json_config
179
     * @return PlaceMapperConfigInterface|NULL
180
     */
181
    private function mapperConfigDecoder(?string $json_config): ?PlaceMapperConfigInterface
182
    {
183
        $config = $json_config === null ? [] : json_decode($json_config, true);
184
        $class = $config['class'] ?? null;
185
        $json_mapper_config = $config['config'] ?? null;
186
        if ($class === null || $json_mapper_config === null) {
187
            return null;
188
        }
189
        try {
190
            $mapper_config = app($class);
191
            if (!$mapper_config instanceof PlaceMapperConfigInterface) {
192
                return null;
193
            }
194
            return $mapper_config->jsonDeserialize($json_mapper_config);
195
        } catch (BindingResolutionException $ex) {
196
            return null;
197
        }
198
    }
199
200
    /**
201
     * Create a MapColorsConfig object from a JSON column value.
202
     * Returns null if the JSON string is invalid, or if the colors are not valid.
203
     *
204
     * @param string $colors_config
205
     * @return MapColorsConfig|NULL
206
     */
207
    private function colorsDecoder(string $colors_config): ?MapColorsConfig
208
    {
209
        $colors = json_decode($colors_config, true);
210
        if (!is_array($colors) && count($colors) !== 4) {
211
            return null;
212
        }
213
        try {
214
            return new MapColorsConfig(
215
                \Spatie\Color\Factory::fromString($colors['default'] ?? ''),
216
                \Spatie\Color\Factory::fromString($colors['stroke'] ?? ''),
217
                \Spatie\Color\Factory::fromString($colors['maxvalue'] ?? ''),
218
                \Spatie\Color\Factory::fromString($colors['hover'] ?? '')
219
            );
220
        } catch (InvalidColorValue $ex) {
221
            return null;
222
        }
223
    }
224
225
    /**
226
     * Get a closure to filter views by enabled/disabled status
227
     *
228
     * @param bool $include_disabled
229
     *
230
     * @return Closure(AbstractGeoAnalysisView $view):bool
231
     */
232
    private function enabledFilter(bool $include_disabled): Closure
233
    {
234
        return function (AbstractGeoAnalysisView $view) use ($include_disabled): bool {
235
            return $include_disabled || $view->isEnabled();
236
        };
237
    }
238
}
239