CoordinatesPlaceMapper::map()   B
last analyzed

Complexity

Conditions 11
Paths 6

Size

Total Lines 35
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 24
nc 6
nop 2
dl 0
loc 35
rs 7.3166
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;
19
use Brick\Geo\Engine\GeometryEngine;
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
    private ?GeometryEngine $geometry_engine = null;
43
44
    /**
45
     * {@inheritDoc}
46
     * @see \MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperInterface::title()
47
     */
48
    public function title(): string
49
    {
50
        return I18N::translate('Mapping on place coordinates');
51
    }
52
53
    /**
54
     * {@inheritDoc}
55
     *
56
     * {@internal The Place is associated to a Point only.
57
     * PlaceLocation can calculate a BoundingBox.
58
     * Using a BoundingBox could make the mapping more complex and potentially arbitary.
59
     * Furthermore, when no coordinate is found for the place or its children, then it bubbles up to the parents.
60
     * This could create the unwanted side effect of a very large area to consider}
61
     *
62
     * @see \MyArtJaub\Webtrees\Contracts\GeoDispersion\PlaceMapperInterface::map()
63
     */
64
    public function map(Place $place, string $feature_property): ?string
65
    {
66
        $location = new PlaceLocation($place->gedcomName());
67
        $longitude = $location->longitude();
68
        $latitude = $location->latitude();
69
        if ($longitude === null || $latitude === null) {
70
            return null;
71
        }
72
73
        $features_index = $this->featuresIndex();
74
        if ($features_index === null) {
75
            return null;
76
        }
77
78
        $place_point = Point::xy($longitude, $latitude, $features_index['SRID']);
79
        $grid_box = $this->getGridCell(
80
            $place_point,
81
            $features_index['map_NE'],
82
            $features_index['map_SW'],
83
            $features_index['nb_columns']
84
        );
85
        if ($grid_box === null || !$this->setGeometryEngine() || $this->geometry_engine == null) {
86
            return null;
87
        }
88
        $features = $features_index['grid'][$grid_box[0]][$grid_box[1]];
89
        foreach ($features as $feature) {
90
            $geometry = $feature->getGeometry();
91
            if (
92
                $geometry !== null && $place_point->SRID() === $geometry->SRID() &&
93
                $this->geometry_engine->contains($geometry, $place_point)
94
            ) {
95
                return $feature->getProperty($feature_property);
96
            }
97
        }
98
        return null;
99
    }
100
101
    /**
102
     * Return the XY coordinates in a bounded grid of the cell containing a specific point.
103
     *
104
     * @param Point $point Point to find
105
     * @param Point $grid_NE North-East point of the bounded grid
106
     * @param Point $grid_SW South-West point fo the bounded grid
107
     * @param int $grid_columns Number of columns/rows in the grid
108
     * @return int[]|NULL
109
     */
110
    protected function getGridCell(Point $point, Point $grid_NE, Point $grid_SW, int $grid_columns): ?array
111
    {
112
        list($x, $y) = $point->toArray();
113
        list($x_max, $y_max) = $grid_NE->toArray();
114
        list($x_min, $y_min) = $grid_SW->toArray();
115
116
        $x_step = ($x_max - $x_min) / $grid_columns;
117
        $y_step = ($y_max - $y_min) / $grid_columns;
118
119
        if ($x_min <= $x && $x <= $x_max && $y_min <= $y && $y <= $y_max) {
120
            return [
121
                $x === $x_max ? $grid_columns - 1 : intval(($x - $x_min) / $x_step),
122
                $y === $y_max ? $grid_columns - 1 : intval(($y - $y_min) / $y_step)
123
            ];
124
        }
125
        return null;
126
    }
127
128
    /**
129
     * Get an indexed array of the features of the map.
130
     *
131
     * {@internal The map is divided in a grid, eacg cell containing the features which bounding box overlaps that cell.
132
     * The grid is computed once for each map, and cached.}
133
     *
134
     * @phpcs:ignore Generic.Files.LineLength.TooLong
135
     * @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
136
     */
137
    protected function featuresIndex(): ?array
138
    {
139
        $cacheKey = $this->cacheKey();
140
        if ($cacheKey === null) {
141
            return null;
142
        }
143
        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...
144
            $map_def = $this->data('map');
145
            if (
146
                !$this->setGeometryEngine()
147
                || $map_def === null
148
                || !($map_def instanceof MapDefinitionInterface)
149
            ) {
150
                return null;
151
            }
152
            $bounding_boxes = [];
153
            $map_bounding_box = new BoundingBox();
154
            $srid = 0;
155
            foreach ($map_def->features() as $feature) {
156
                $geometry = $feature->getGeometry();
157
                if ($geometry === null) {
158
                    continue;
159
                }
160
                $srid = $geometry->SRID();
161
                $bounding_box = $geometry->getBoundingBox();
162
                $bounding_boxes[] = [$feature, $bounding_box];
163
                $map_bounding_box = $map_bounding_box->extendedWithBoundingBox($bounding_box);
164
            }
165
            $grid_columns = count($bounding_boxes);
166
            $grid = array_fill(0, $grid_columns, array_fill(0, $grid_columns, []));
167
            $map_NE = $map_bounding_box->getNorthEast();
168
            $map_SW = $map_bounding_box->getSouthWest();
169
            foreach ($bounding_boxes as $item) {
170
                $grid_box_SW = $this->getGridCell($item[1]->getSouthWest(), $map_NE, $map_SW, $grid_columns) ?? [1, 1];
171
                $grid_box_NE = $this->getGridCell($item[1]->getNorthEast(), $map_NE, $map_SW, $grid_columns) ?? [0, 0];
172
                for ($i = $grid_box_SW[0]; $i <= $grid_box_NE[0]; $i++) {
173
                    for ($j = $grid_box_SW[1]; $j <= $grid_box_NE[1]; $j++) {
174
                        $grid[$i][$j][] = $item[0];
175
                    }
176
                }
177
            }
178
            return [
179
                'grid'          =>  $grid,
180
                'nb_columns'    =>  $grid_columns,
181
                'map_NE'        =>  $map_NE,
182
                'map_SW'        =>  $map_SW,
183
                'SRID'          =>  $srid
184
            ];
185
        });
186
    }
187
188
    /**
189
     * Set the Brick Geo Engine to use the database for geospatial computations.
190
     * The engine is set only if it has not been set beforehand.
191
     *
192
     * @return bool
193
     */
194
    protected function setGeometryEngine(): bool
195
    {
196
        try {
197
            if ($this->geometry_engine === null) {
198
                $this->geometry_engine = new PDOEngine(DB::connection()->getPdo());
199
            }
200
            $point = Point::xy(1, 1);
201
            return $this->geometry_engine->equals($point, $point);
0 ignored issues
show
Bug introduced by
The method equals() does not exist on null. ( Ignorable by Annotation )

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

201
            return $this->geometry_engine->/** @scrutinizer ignore-call */ equals($point, $point);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
202
        } catch (Throwable $ex) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
203
        }
204
        return false;
205
    }
206
207
    /**
208
     * Get the key to cache the indexed grid of features.
209
     *
210
     * @return string|NULL
211
     */
212
    protected function cacheKey(): ?string
213
    {
214
        if ($this->cache_key === null) {
215
            $map_def = $this->data('map');
216
            if ($map_def === null || !($map_def instanceof MapDefinitionInterface)) {
217
                return null;
218
            }
219
            return spl_object_id($this) . '-map-' . $map_def->id();
220
        }
221
        return $this->cache_key;
222
    }
223
}
224