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