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

CoordinatesPlaceMapper::featuresIndex()   B

Complexity

Conditions 10
Paths 2

Size

Total Lines 47
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

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

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