Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
12.50% covered (danger)
12.50%
9 / 72
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryGeoSearchDb
12.50% covered (danger)
12.50%
9 / 72
25.00% covered (danger)
25.00%
1 / 4
346.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
272
 addCoordFilter
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 intRange
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace GeoData\Api;
4
5use GeoData\BoundingBox;
6use GeoData\Coord;
7use GeoData\Globe;
8use MediaWiki\Api\ApiPageSet;
9use MediaWiki\Api\ApiQuery;
10use MediaWiki\Title\Title;
11
12class QueryGeoSearchDb extends QueryGeoSearch {
13    public function __construct(
14        ApiQuery $query,
15        string $moduleName,
16    ) {
17        parent::__construct( $query, $moduleName );
18    }
19
20    /**
21     * @param ApiPageSet|null $resultPageSet
22     */
23    protected function run( $resultPageSet = null ): void {
24        parent::run( $resultPageSet );
25        $params = $this->extractRequestParams();
26
27        if ( $params['sort'] === 'relevance' ) {
28            $this->dieWithError( 'apierror-geodata-norelevancesort', 'no-relevance-sort' );
29        }
30
31        $this->addTables( 'geo_tags' );
32        $this->addFields( [ 'gt_lat', 'gt_lon', 'gt_primary' ] );
33        foreach ( $params['prop'] as $prop ) {
34            if ( isset( Coord::FIELD_MAPPING[$prop] ) ) {
35                $this->addFields( Coord::FIELD_MAPPING[$prop] );
36            }
37        }
38        $this->addWhereFld( 'gt_globe', $this->coord->globe );
39        $this->addWhere( 'gt_page_id = page_id' );
40        $dbr = $this->getDB();
41        if ( $this->idToExclude ) {
42            $this->addWhere( $dbr->expr( 'gt_page_id', '!=', $this->idToExclude ) );
43        }
44        if ( isset( $params['maxdim'] ) ) {
45            $this->addWhere( $dbr->expr( 'gt_dim', '<', intval( $params['maxdim'] ) ) );
46        }
47        $primary = $params['primary'];
48        $this->addWhereIf( [ 'gt_primary' => intval( $primary === 'primary' ) ], $primary !== 'all' );
49
50        $this->addCoordFilter();
51
52        $limit = $params['limit'];
53
54        $res = $this->select( __METHOD__ );
55
56        $rows = [];
57        foreach ( $res as $row ) {
58            $row->dist = $this->coord->distanceTo( Coord::newFromRow( $row ) );
59            $rows[] = $row;
60        }
61        // sort in PHP because sorting via SQL would involve a filesort
62        usort( $rows, static function ( $row1, $row2 ) {
63            return $row1->dist - $row2->dist;
64        } );
65        $result = $this->getResult();
66        foreach ( $rows as $row ) {
67            if ( !$limit-- ) {
68                break;
69            }
70            if ( $resultPageSet === null ) {
71                $title = Title::newFromRow( $row );
72                $vals = [
73                    'pageid' => intval( $row->page_id ),
74                    'ns' => $title->getNamespace(),
75                    'title' => $title->getPrefixedText(),
76                    'lat' => floatval( $row->gt_lat ),
77                    'lon' => floatval( $row->gt_lon ),
78                    'dist' => round( $row->dist, 1 ),
79                    'primary' => boolval( $row->gt_primary ),
80                ];
81                foreach ( $params['prop'] as $prop ) {
82                    $column = Coord::FIELD_MAPPING[$prop] ?? null;
83                    if ( $column && isset( $row->$column ) ) {
84                        // Don't output default globe
85                        if ( !( $prop === 'globe' && $row->$column === Globe::EARTH ) ) {
86                            $vals[$prop] = $row->$column;
87                        }
88                    }
89                }
90                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $vals );
91                if ( !$fit ) {
92                    break;
93                }
94            } else {
95                $resultPageSet->processDbRow( $row );
96            }
97        }
98    }
99
100    protected function addCoordFilter(): void {
101        $bbox = $this->bbox ?: BoundingBox::newFromRadius( $this->coord, $this->radius );
102        $coord1 = $bbox->topLeft();
103        $coord2 = $bbox->bottomRight();
104        $this->addWhereFld( 'gt_lat_int', $this->intRange( $coord1->lat, $coord2->lat ) );
105        $this->addWhereFld( 'gt_lon_int', $this->intRange( $coord1->lon, $coord2->lon ) );
106
107        $this->addWhereRange( 'gt_lat', 'newer', (string)$coord1->lat, (string)$coord2->lat, false );
108        if ( $coord1->lon > $coord2->lon ) {
109            $this->addWhere( "gt_lon < {$coord2->lon} OR gt_lon > {$coord1->lon}" );
110        } else {
111            $this->addWhereRange( 'gt_lon', 'newer', (string)$coord1->lon, (string)$coord2->lon, false );
112        }
113        $this->addOption( 'USE INDEX', [ 'geo_tags' => 'gt_spatial' ] );
114    }
115
116    /**
117     * Returns a range of tenths of degree
118     *
119     * @param float $start
120     * @param float $end
121     * @return int[]
122     */
123    public function intRange( float $start, float $end ): array {
124        $granularity = $this->getConfig()->get( 'GeoDataIndexGranularity' );
125        $start = round( $start * $granularity );
126        $end = round( $end * $granularity );
127        // @todo: works only on Earth
128        if ( $start > $end ) {
129            return array_merge(
130                range( -180 * $granularity, $end ),
131                range( $start, 180 * $granularity )
132            );
133        } else {
134            return range( $start, $end );
135        }
136    }
137}