Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
14.29% |
10 / 70 |
|
25.00% |
1 / 4 |
CRAP | |
0.00% |
0 / 1 |
QueryGeoSearchDb | |
14.29% |
10 / 70 |
|
25.00% |
1 / 4 |
356.13 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
272 | |||
addCoordFilter | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
intRange | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace GeoData\Api; |
4 | |
5 | use ApiPageSet; |
6 | use ApiQuery; |
7 | use GeoData\Coord; |
8 | use GeoData\Math; |
9 | use MediaWiki\Title\Title; |
10 | |
11 | class 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 | } |