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