Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.15% |
68 / 73 |
|
66.67% |
2 / 3 |
CRAP | |
0.00% |
0 / 1 |
CirrusGeoFeature | |
93.15% |
68 / 73 |
|
66.67% |
2 / 3 |
15.07 | |
0.00% |
0 / 1 |
parseGeoNearbyTitle | |
85.29% |
29 / 34 |
|
0.00% |
0 / 1 |
8.20 | |||
parseGeoNearby | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
5 | |||
parseDistance | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace GeoData\Search; |
4 | |
5 | use CirrusSearch\WarningCollector; |
6 | use GeoData\Coord; |
7 | use GeoData\GeoData; |
8 | use GeoData\Globe; |
9 | use MediaWiki\MediaWikiServices; |
10 | |
11 | /** |
12 | * Trait for geo based features. |
13 | */ |
14 | trait CirrusGeoFeature { |
15 | /** @var int Default radius, in meters */ |
16 | private static $DEFAULT_RADIUS = 5000; |
17 | |
18 | /** |
19 | * radius, if provided, must have either m or km suffix. Valid formats: |
20 | * <title> |
21 | * <radius>,<title> |
22 | * |
23 | * @param WarningCollector $warningCollector |
24 | * @param string $key Key used to trigger feature |
25 | * @param string $text user input to parse |
26 | * @return array{?Coord,int,int|string} Three member array with Coordinate object, integer radius |
27 | * in meters, and page id to exclude from results.. When invalid the |
28 | * Coordinate returned will be null. |
29 | */ |
30 | public function parseGeoNearbyTitle( WarningCollector $warningCollector, $key, $text ): array { |
31 | $titleFactory = MediaWikiServices::getInstance()->getTitleFactory(); |
32 | $title = $titleFactory->newFromText( $text ); |
33 | if ( $title && $title->exists() ) { |
34 | // Default radius if not provided: 5km |
35 | $radius = self::$DEFAULT_RADIUS; |
36 | } else { |
37 | // If the provided value is not a title try to extract a radius prefix |
38 | // from the beginning. If $text has a valid radius prefix see if the |
39 | // remaining text is a valid title to use. |
40 | $pieces = explode( ',', $text, 2 ); |
41 | if ( count( $pieces ) !== 2 ) { |
42 | $warningCollector->addWarning( |
43 | "geodata-search-feature-invalid-coordinates", |
44 | $key, $text |
45 | ); |
46 | return [ null, 0, '' ]; |
47 | } |
48 | $radius = self::parseDistance( $pieces[0] ); |
49 | if ( $radius === null ) { |
50 | $warningCollector->addWarning( |
51 | "geodata-search-feature-invalid-distance", |
52 | $key, $pieces[0] |
53 | ); |
54 | return [ null, 0, '' ]; |
55 | } |
56 | $title = $titleFactory->newFromText( $pieces[1] ); |
57 | if ( !$title || !$title->exists() ) { |
58 | $warningCollector->addWarning( |
59 | "geodata-search-feature-unknown-title", |
60 | $key, $pieces[1] |
61 | ); |
62 | return [ null, 0, '' ]; |
63 | } |
64 | } |
65 | |
66 | $pageId = $title->getArticleID(); |
67 | $coord = GeoData::getPageCoordinates( $pageId ); |
68 | if ( !$coord ) { |
69 | $warningCollector->addWarning( |
70 | 'geodata-search-feature-title-no-coordinates', |
71 | (string)$title |
72 | ); |
73 | return [ null, 0, '' ]; |
74 | } |
75 | |
76 | return [ $coord, $radius, $pageId ]; |
77 | } |
78 | |
79 | /** |
80 | * radius, if provided, must have either m or km suffix. Latitude and longitude |
81 | * must be floats in the domain of [-90:90] for latitude and [-180,180] for |
82 | * longitude. Valid formats: |
83 | * <lat>,<lon> |
84 | * <radius>,<lat>,<lon> |
85 | * |
86 | * @param WarningCollector $warningCollector |
87 | * @param string $key |
88 | * @param string $text |
89 | * @return array{array{lat:float,lon:float,globe:string}|null,int} Two member array with |
90 | * coordinates and integer radius in meters. When invalid the coordinates will be null. |
91 | */ |
92 | public function parseGeoNearby( |
93 | WarningCollector $warningCollector, |
94 | $key, |
95 | $text |
96 | ) { |
97 | $pieces = explode( ',', $text, 3 ); |
98 | // Default radius if not provided: 5km |
99 | $radius = self::$DEFAULT_RADIUS; |
100 | if ( count( $pieces ) === 3 ) { |
101 | $radius = self::parseDistance( $pieces[0] ); |
102 | if ( $radius === null ) { |
103 | $warningCollector->addWarning( |
104 | 'geodata-search-feature-invalid-distance', |
105 | $key, $pieces[0] |
106 | ); |
107 | return [ null, 0 ]; |
108 | } |
109 | [ , $lat, $lon ] = $pieces; |
110 | } elseif ( count( $pieces ) === 2 ) { |
111 | [ $lat, $lon ] = $pieces; |
112 | } else { |
113 | $warningCollector->addWarning( |
114 | 'geodata-search-feature-invalid-coordinates', |
115 | $key, $text |
116 | ); |
117 | return [ null, 0 ]; |
118 | } |
119 | |
120 | $globe = new Globe( Globe::EARTH ); |
121 | if ( !$globe->coordinatesAreValid( $lat, $lon ) ) { |
122 | $warningCollector->addWarning( |
123 | 'geodata-search-feature-invalid-coordinates', |
124 | $key, $text |
125 | ); |
126 | return [ null, 0 ]; |
127 | } |
128 | |
129 | return [ |
130 | [ 'lat' => floatval( $lat ), 'lon' => floatval( $lon ), 'globe' => $globe->getName() ], |
131 | $radius, |
132 | ]; |
133 | } |
134 | |
135 | /** |
136 | * @param string $distance |
137 | * @return int|null Parsed distance in meters, or null if unparsable |
138 | */ |
139 | public static function parseDistance( $distance ) { |
140 | if ( !preg_match( '/^(\d+)(m|km|mi|ft|yd)$/', $distance, $matches ) ) { |
141 | return null; |
142 | } |
143 | |
144 | $scale = [ |
145 | 'm' => 1, |
146 | 'km' => 1000, |
147 | // Supported non-SI units, and their conversions, sourced from |
148 | // https://en.wikipedia.org/wiki/Unit_of_length#Imperial.2FUS |
149 | 'mi' => 1609.344, |
150 | 'ft' => 0.3048, |
151 | 'yd' => 0.9144, |
152 | ]; |
153 | |
154 | return max( 10, (int)round( (int)$matches[1] * $scale[$matches[2]] ) ); |
155 | } |
156 | } |