Passed
Push — feature/code-analysis ( e964aa...4fe35d )
by Jonathan
14:33
created

CoordinatesPlaceMapper::featuresIndex()   B

Complexity

Conditions 10
Paths 2

Size

Total Lines 47
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 37
nc 2
nop 0
dl 0
loc 47
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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-2022, 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\PlaceMappers;
16
17
use Brick\Geo\BoundingBox;
18
use Brick\Geo\Point;
0 ignored issues
show
Bug introduced by
The type Brick\Geo\Point was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use Brick\Geo\Engine\GeometryEngineRegistry;
20
use Brick\Geo\Engine\PDOEngine;
21
use Fisharebest\Webtrees\I18N;
22
use Fisharebest\Webtrees\Place;
23
use Fisharebest\Webtrees\PlaceLocation;
24
use Fisharebest\Webtrees\Registry;
25
use Illuminate\Database\Capsule\Manager as DB;
26
use MyArtJaub\Webtrees\Contracts\GeoDispersion\MapDefinitionInterface;
27
use MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperInterface;
28
use Throwable;
29
30
/**
31
 * Mapper using coordinated to map a location to a GeoJson map feature.
32
 * This use the PlaceLocation table to determine the coordinates of the place, through the core PlaceLocation object.
33
 *
34
 * {@internal This mapper is indexing the features based on a grid to optimise the performances.
35
 * Using the geospatial `contains` (SQL `ST_contains`) method naively is a lot slower.}
36
 */
37
class CoordinatesPlaceMapper implements PlaceMapperInterface
38
{
39
    use PlaceMapperTrait;
40
41
    private ?string $cache_key = null;
42
43
    /**
44
     * {@inheritDoc}
45
     * @see \MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperInterface::title()
46
     */
47
    public function title(): string
48
    {
49
        return I18N::translate('Mapping on place coordinates');
50
    }
51
52
    /**
53
     * {@inheritDoc}
54
     *
55
     * {@internal The Place is associated to a Point only.
56
     * PlaceLocation can calculate a BoundingBox.
57
     * Using a BoundingBox could make the mapping more complex and potentially arbitary.
58
     * Furthermore, when no coordinate is found for the place or its children, then it bubbles up to the parents.
59
     * This could create the unwanted side effect of a very large area to consider}
60
     *
61
     * @see \MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperInterface::map()
62
     */
63
    public function map(Place $place, string $feature_property): ?string
64
    {
65
        $location = new PlaceLocation($place->gedcomName());
66
        $longitude = $location->longitude();
67
        $latitude = $location->latitude();
68
        if ($longitude === null || $latitude === null) {
69
            return null;
70
        }
71
72
        $features_index = $this->featuresIndex();
73
        if ($features_index === null) {
74
            return null;
75
        }
76
77
        $place_point = Point::xy($longitude, $latitude, $features_index['SRID']);
78
        $grid_box = $this->getGridCell(
79
            $place_point,
80
            $features_index['map_NE'],
81
            $features_index['map_SW'],
82
            $features_index['nb_columns']
83
        );
84
        if ($grid_box === null || !$this->setGeometryEngine()) {
85
            return null;
86
        }
87
        $features = $features_index['grid'][$grid_box[0]][$grid_box[1]];
88
        foreach ($features as $feature) {
89
            $geometry = $feature->getGeometry();
90
            if ($geometry !== null && $place_point->SRID() === $geometry->SRID() && $geometry->contains($place_point)) {
91
                return $feature->getProperty($feature_property);
92
            }
93
        }
94
        return null;
95
    }
96
97
    /**
98
     * Return the XY coordinates in a bounded grid of the cell containing a specific point.
99
     *
100
     * @param Point $point Point to find
101
     * @param Point $grid_NE North-East point of the bounded grid
102
     * @param Point $grid_SW South-West point fo the bounded grid
103
     * @param int $grid_columns Number of columns/rows in the grid
104
     * @return int[]|NULL
105
     */
106
    protected function getGridCell(Point $point, Point $grid_NE, Point $grid_SW, int $grid_columns): ?array
107
    {
108
        list($x, $y) = $point->toArray();
109
        list($x_max, $y_max) = $grid_NE->toArray();
110
        list($x_min, $y_min) = $grid_SW->toArray();
111
112
        $x_step = ($x_max - $x_min) / $grid_columns;
113
        $y_step = ($y_max - $y_min) / $grid_columns;
114
115
        if ($x_min <= $x && $x <= $x_max && $y_min <= $y && $y <= $y_max) {
116
            return [
117
                $x === $x_max ? $grid_columns - 1 : intval(($x - $x_min) / $x_step),
118
                $y === $y_max ? $grid_columns - 1 : intval(($y - $y_min) / $y_step)
119
            ];
120
        }
121
        return null;
122
    }
123
124
    /**
125
     * Get an indexed array of the features of the map.
126
     *
127
     * {@internal The map is divided in a grid, eacg cell containing the features which bounding box overlaps that cell.
128
     * The grid is computed once for each map, and cached.}
129
     *
130
     * @phpcs:ignore Generic.Files.LineLength.TooLong
131
     * @return array{grid: array<int, array<int, \Brick\Geo\IO\GeoJSON\Feature[]>>, nb_columns: int, map_NE: \Brick\Geo\Point, map_SW: \Brick\Geo\Point, SRID: int}|NULL
132
     */
133
    protected function featuresIndex(): ?array
134
    {
135
        $cacheKey = $this->cacheKey();
136
        if ($cacheKey === null) {
137
            return null;
138
        }
139
        return Registry::cache()->array()->remember($cacheKey, function (): ?array {
0 ignored issues
show
Bug Best Practice introduced by
The expression return Fisharebest\Webtr...ion(...) { /* ... */ }) returns the type Fisharebest\Webtrees\T which is incompatible with the type-hinted return array|null.
Loading history...
140
            $map_def = $this->data('map');
141
            if (
142
                !$this->setGeometryEngine()
143
                || $map_def === null
144
                || !($map_def instanceof MapDefinitionInterface)
145
            ) {
146
                return null;
147
            }
148
            $bounding_boxes = [];
149
            $map_bounding_box = new BoundingBox();
150
            $srid = 0;
151
            foreach ($map_def->features() as $feature) {
152
                $geometry = $feature->getGeometry();
153
                if ($geometry === null) {
154
                    continue;
155
                }
156
                $srid = $geometry->SRID();
157
                $bounding_box = $geometry->getBoundingBox();
158
                $bounding_boxes[] = [$feature, $bounding_box];
159
                $map_bounding_box = $map_bounding_box->extendedWithBoundingBox($bounding_box);
160
            }
161
            $grid_columns = count($bounding_boxes);
162
            $grid = array_fill(0, $grid_columns, array_fill(0, $grid_columns, []));
163
            $map_NE = $map_bounding_box->getNorthEast();
164
            $map_SW = $map_bounding_box->getSouthWest();
165
            foreach ($bounding_boxes as $item) {
166
                $grid_box_SW = $this->getGridCell($item[1]->getSouthWest(), $map_NE, $map_SW, $grid_columns) ?? [1, 1];
167
                $grid_box_NE = $this->getGridCell($item[1]->getNorthEast(), $map_NE, $map_SW, $grid_columns) ?? [0, 0];
168
                for ($i = $grid_box_SW[0]; $i <= $grid_box_NE[0]; $i++) {
169
                    for ($j = $grid_box_SW[1]; $j <= $grid_box_NE[1]; $j++) {
170
                        $grid[$i][$j][] = $item[0];
171
                    }
172
                }
173
            }
174
            return [
175
                'grid'          =>  $grid,
176
                'nb_columns'    =>  $grid_columns,
177
                'map_NE'        =>  $map_NE,
178
                'map_SW'        =>  $map_SW,
179
                'SRID'          =>  $srid
180
            ];
181
        });
182
    }
183
184
    /**
185
     * Set the Brick Geo Engine to use the database for geospatial computations.
186
     * The engine is set only if it has not been set beforehand.
187
     *
188
     * @return bool
189
     */
190
    protected function setGeometryEngine(): bool
191
    {
192
        try {
193
            if (!GeometryEngineRegistry::has()) {
194
                GeometryEngineRegistry::set(new PDOEngine(DB::connection()->getPdo()));
195
            }
196
            $point = Point::xy(1, 1);
197
            return $point->equals($point);
198
        } catch (Throwable $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
199
        }
200
        return false;
201
    }
202
203
    /**
204
     * Get the key to cache the indexed grid of features.
205
     *
206
     * @return string|NULL
207
     */
208
    protected function cacheKey(): ?string
209
    {
210
        if ($this->cache_key === null) {
211
            $map_def = $this->data('map');
212
            if ($map_def === null || !($map_def instanceof MapDefinitionInterface)) {
213
                return null;
214
            }
215
            return spl_object_id($this) . '-map-' . $map_def->id();
216
        }
217
        return $this->cache_key;
218
    }
219
}
220