Completed
Push — ezp-31420-merge-up ( ec14fb...141a64 )
by
unknown
40:13 queued 27:42
created

MapLocationDistance::handle()   C

Complexity

Conditions 13
Paths 71

Size

Total Lines 137

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
nc 71
nop 4
dl 0
loc 137
rs 5.2933
c 0
b 0
f 0

How to fix   Long Method    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
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
5
 * @license For full copyright and license information view LICENSE file distributed with this source code.
6
 */
7
namespace eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriterionHandler;
8
9
use eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter;
10
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
11
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Value\MapLocationValue;
12
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
13
use eZ\Publish\Core\Persistence\Database\SelectQuery;
14
use RuntimeException;
15
16
/**
17
 * MapLocationDistance criterion handler.
18
 */
19
class MapLocationDistance extends FieldBase
20
{
21
    /**
22
     * Distance in kilometers of one degree longitude at the Equator.
23
     */
24
    const DEGREE_KM = 111.195;
25
26
    /**
27
     * Radius of the planet in kilometers.
28
     */
29
    const EARTH_RADIUS = 6371.01;
30
31
    /**
32
     * Check if this criterion handler accepts to handle the given criterion.
33
     *
34
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
35
     *
36
     * @return bool
37
     */
38
    public function accept(Criterion $criterion)
39
    {
40
        return $criterion instanceof Criterion\MapLocationDistance;
41
    }
42
43
    /**
44
     * Returns a list of IDs of searchable FieldDefinitions for the given criterion target.
45
     *
46
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException If no searchable fields are found for the given $fieldIdentifier.
47
     *
48
     * @param string $fieldIdentifier
49
     *
50
     * @return array
51
     */
52
    protected function getFieldDefinitionIds($fieldIdentifier)
53
    {
54
        $fieldDefinitionIdList = [];
55
        $fieldMap = $this->contentTypeHandler->getSearchableFieldMap();
56
57
        foreach ($fieldMap as $contentTypeIdentifier => $fieldIdentifierMap) {
58
            // First check if field exists in the current ContentType, there is nothing to do if it doesn't
59
            if (
60
                !(
61
                    isset($fieldIdentifierMap[$fieldIdentifier])
62
                    && $fieldIdentifierMap[$fieldIdentifier]['field_type_identifier'] === 'ezgmaplocation'
63
                )
64
            ) {
65
                continue;
66
            }
67
68
            $fieldDefinitionIdList[] = $fieldIdentifierMap[$fieldIdentifier]['field_definition_id'];
69
        }
70
71
        if (empty($fieldDefinitionIdList)) {
72
            throw new InvalidArgumentException(
73
                '$criterion->target',
74
                "No searchable Fields found for the provided Criterion target '{$fieldIdentifier}'."
75
            );
76
        }
77
78
        return $fieldDefinitionIdList;
79
    }
80
81
    protected function kilometersToDegrees($kilometers)
82
    {
83
        return $kilometers / self::DEGREE_KM;
84
    }
85
86
    /**
87
     * Generate query expression for a Criterion this handler accepts.
88
     *
89
     * accept() must be called before calling this method.
90
     *
91
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException If no searchable fields are found for the given criterion target.
92
     * @throws \RuntimeException If given criterion operator is not handled
93
     *
94
     * @param \eZ\Publish\Core\Search\Legacy\Content\Common\Gateway\CriteriaConverter $converter
95
     * @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
96
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
97
     * @param array $languageSettings
98
     *
99
     * @return \eZ\Publish\Core\Persistence\Database\Expression
100
     */
101
    public function handle(
102
        CriteriaConverter $converter,
103
        SelectQuery $query,
104
        Criterion $criterion,
105
        array $languageSettings
106
    ) {
107
        $fieldDefinitionIds = $this->getFieldDefinitionIds($criterion->target);
108
        $subSelect = $query->subSelect();
109
110
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\Criterion\Value\MapLocationValue $location */
111
        $location = $criterion->valueData;
112
113
        /*
114
         * Note: this formula is precise only for short distances.
115
         * @todo if ABS function was available in Zeta Database component it should be possible to account for
116
         * distances across the date line. Revisit when Doctrine DBAL is introduced.
117
         */
118
        $longitudeCorrectionByLatitude = cos(deg2rad($location->latitude)) ** 2;
119
        $distanceExpression = $subSelect->expr->add(
120
            $subSelect->expr->mul(
121
                $subSelect->expr->sub(
122
                    $this->dbHandler->quoteColumn('latitude', 'ezgmaplocation'),
123
                    $subSelect->bindValue($location->latitude)
124
                ),
125
                $subSelect->expr->sub(
126
                    $this->dbHandler->quoteColumn('latitude', 'ezgmaplocation'),
127
                    $subSelect->bindValue($location->latitude)
128
                )
129
            ),
130
            $subSelect->expr->mul(
131
                $subSelect->expr->sub(
132
                    $this->dbHandler->quoteColumn('longitude', 'ezgmaplocation'),
133
                    $subSelect->bindValue($location->longitude)
134
                ),
135
                $subSelect->expr->sub(
136
                    $this->dbHandler->quoteColumn('longitude', 'ezgmaplocation'),
137
                    $subSelect->bindValue($location->longitude)
138
                ),
139
                $subSelect->bindValue($longitudeCorrectionByLatitude)
140
            )
141
        );
142
143
        switch ($criterion->operator) {
144
            case Criterion\Operator::IN:
145
            case Criterion\Operator::EQ:
146
            case Criterion\Operator::GT:
147
            case Criterion\Operator::GTE:
148
            case Criterion\Operator::LT:
149
            case Criterion\Operator::LTE:
150
                $operatorFunction = $this->comparatorMap[$criterion->operator];
151
                $distanceInDegrees = $this->kilometersToDegrees($criterion->value) ** 2;
152
                $distanceFilter = $subSelect->expr->$operatorFunction(
153
                    $distanceExpression,
154
                    $subSelect->expr->round(
155
                        $subSelect->bindValue($distanceInDegrees),
156
                        10
157
                    )
158
                );
159
                break;
160
161
            case Criterion\Operator::BETWEEN:
162
                $distanceInDegrees1 = $this->kilometersToDegrees($criterion->value[0]) ** 2;
163
                $distanceInDegrees2 = $this->kilometersToDegrees($criterion->value[1]) ** 2;
164
                $distanceFilter = $subSelect->expr->between(
165
                    $distanceExpression,
166
                    $subSelect->expr->round(
167
                        $subSelect->bindValue($distanceInDegrees1),
168
                        10
169
                    ),
170
                    $subSelect->expr->round(
171
                        $subSelect->bindValue($distanceInDegrees2),
172
                        10
173
                    )
174
                );
175
                break;
176
177
            default:
178
                throw new RuntimeException('Unknown operator.');
179
        }
180
181
        // Calculate bounding box if possible
182
        // @todo consider covering operators EQ and IN as well
183
        $boundingConstraints = [];
184
        switch ($criterion->operator) {
185
            case Criterion\Operator::LT:
186
            case Criterion\Operator::LTE:
187
                $distanceUpper = $criterion->value;
188
                break;
189
            case Criterion\Operator::BETWEEN:
190
                $distanceUpper = $criterion->value[0] > $criterion->value[1] ?
191
                    $criterion->value[0] :
192
                    $criterion->value[1];
193
                break;
194
        }
195
        if (isset($distanceUpper)) {
196
            $boundingConstraints = $this->getBoundingConstraints($subSelect, $location, $distanceUpper);
197
        }
198
199
        $subSelect
200
            ->select($this->dbHandler->quoteColumn('contentobject_id'))
201
            ->from($this->dbHandler->quoteTable('ezcontentobject_attribute'))
202
            ->innerJoin(
203
                $this->dbHandler->quoteTable('ezgmaplocation'),
204
                $subSelect->expr->lAnd(
205
                    [
206
                        $subSelect->expr->eq(
207
                            $this->dbHandler->quoteColumn('contentobject_version', 'ezgmaplocation'),
208
                            $this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute')
209
                        ),
210
                        $subSelect->expr->eq(
211
                            $this->dbHandler->quoteColumn('contentobject_attribute_id', 'ezgmaplocation'),
212
                            $this->dbHandler->quoteColumn('id', 'ezcontentobject_attribute')
213
                        ),
214
                    ],
215
                    $boundingConstraints
216
                )
217
            )
218
            ->where(
219
                $subSelect->expr->lAnd(
220
                    $subSelect->expr->eq(
221
                        $this->dbHandler->quoteColumn('version', 'ezcontentobject_attribute'),
222
                        $this->dbHandler->quoteColumn('current_version', 'ezcontentobject')
223
                    ),
224
                    $subSelect->expr->in(
225
                        $this->dbHandler->quoteColumn('contentclassattribute_id', 'ezcontentobject_attribute'),
226
                        $fieldDefinitionIds
227
                    ),
228
                    $distanceFilter,
229
                    $this->getFieldCondition($subSelect, $languageSettings)
230
                )
231
            );
232
233
        return $query->expr->in(
234
            $this->dbHandler->quoteColumn('id', 'ezcontentobject'),
235
            $subSelect
236
        );
237
    }
238
239
    /**
240
     * Credit for the formula goes to http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates.
241
     *
242
     * @param \eZ\Publish\Core\Persistence\Database\SelectQuery $query
243
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion\Value\MapLocationValue $location
244
     * @param float $distance
245
     *
246
     * @return array
247
     */
248
    protected function getBoundingConstraints(SelectQuery $query, MapLocationValue $location, $distance)
249
    {
250
        $boundingCoordinates = $this->getBoundingCoordinates($location, $distance);
251
252
        return [
253
            $query->expr->gte(
254
                $this->dbHandler->quoteColumn('latitude', 'ezgmaplocation'),
255
                $query->bindValue($boundingCoordinates['lowLatitude'])
256
            ),
257
            $query->expr->gte(
258
                $this->dbHandler->quoteColumn('longitude', 'ezgmaplocation'),
259
                $query->bindValue($boundingCoordinates['lowLongitude'])
260
            ),
261
            $query->expr->lte(
262
                $this->dbHandler->quoteColumn('latitude', 'ezgmaplocation'),
263
                $query->bindValue($boundingCoordinates['highLatitude'])
264
            ),
265
            $query->expr->lte(
266
                $this->dbHandler->quoteColumn('longitude', 'ezgmaplocation'),
267
                $query->bindValue($boundingCoordinates['highLongitude'])
268
            ),
269
        ];
270
    }
271
272
    /**
273
     * Calculates and returns bounding box coordinates.
274
     *
275
     * Credits: http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates
276
     *
277
     * @todo it should also be possible to calculate inner bounding box, which could be applied for the
278
     * operators GT, GTE and lower distance of the BETWEEN operator.
279
     *
280
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion\Value\MapLocationValue $location
281
     * @param float $distance
282
     *
283
     * @return array
284
     */
285
    protected function getBoundingCoordinates(MapLocationValue $location, $distance)
286
    {
287
        $radiansLatitude = deg2rad($location->latitude);
288
        $radiansLongitude = deg2rad($location->longitude);
289
        $angularDistance = $distance / self::EARTH_RADIUS;
290
        $deltaLongitude = asin(sin($angularDistance) / cos($radiansLatitude));
291
292
        $lowLatitudeRadians = $radiansLatitude - $angularDistance;
293
        $highLatitudeRadians = $radiansLatitude + $angularDistance;
294
295
        // Check that bounding box does not include poles.
296
        if ($lowLatitudeRadians > -M_PI_2 && $highLatitudeRadians < M_PI_2) {
297
            $boundingCoordinates = [
298
                'lowLatitude' => rad2deg($lowLatitudeRadians),
299
                'lowLongitude' => rad2deg($radiansLongitude - $deltaLongitude),
300
                'highLatitude' => rad2deg($highLatitudeRadians),
301
                'highLongitude' => rad2deg($radiansLongitude + $deltaLongitude),
302
            ];
303
        } else {
304
            // Handle the pole(s) being inside a bounding box, in this case we MUST cover
305
            // full circle of Earth's longitude and one or both poles.
306
            // Note that calculation for distances over the polar regions with flat Earth formula
307
            // will be VERY imprecise.
308
            $boundingCoordinates = [
309
                'lowLatitude' => rad2deg(max($lowLatitudeRadians, -M_PI_2)),
310
                'lowLongitude' => rad2deg(-M_PI),
311
                'highLatitude' => rad2deg(min($highLatitudeRadians, M_PI_2)),
312
                'highLongitude' => rad2deg(M_PI),
313
            ];
314
        }
315
316
        return $boundingCoordinates;
317
    }
318
}
319