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\GeoData; |
7 | use GeoData\Globe; |
8 | use MediaWiki\Config\Config; |
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 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 ) { |
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 Config $config |
88 | * @param string $key |
89 | * @param string $text |
90 | * @return array Two member array with Coordinate object, and integer radius |
91 | * in meters. When invalid the Coordinate returned will be null. |
92 | */ |
93 | public function parseGeoNearby( |
94 | WarningCollector $warningCollector, |
95 | Config $config, |
96 | $key, |
97 | $text |
98 | ) { |
99 | $pieces = explode( ',', $text, 3 ); |
100 | // Default radius if not provided: 5km |
101 | $radius = self::$DEFAULT_RADIUS; |
102 | if ( count( $pieces ) === 3 ) { |
103 | $radius = self::parseDistance( $pieces[0] ); |
104 | if ( $radius === null ) { |
105 | $warningCollector->addWarning( |
106 | 'geodata-search-feature-invalid-distance', |
107 | $key, $pieces[0] |
108 | ); |
109 | return [ null, 0 ]; |
110 | } |
111 | [ , $lat, $lon ] = $pieces; |
112 | } elseif ( count( $pieces ) === 2 ) { |
113 | [ $lat, $lon ] = $pieces; |
114 | } else { |
115 | $warningCollector->addWarning( |
116 | 'geodata-search-feature-invalid-coordinates', |
117 | $key, $text |
118 | ); |
119 | return [ null, 0 ]; |
120 | } |
121 | |
122 | $globe = new Globe( $config->get( 'DefaultGlobe' ) ); |
123 | if ( !$globe->coordinatesAreValid( $lat, $lon ) ) { |
124 | $warningCollector->addWarning( |
125 | 'geodata-search-feature-invalid-coordinates', |
126 | $key, $text |
127 | ); |
128 | return [ null, 0 ]; |
129 | } |
130 | |
131 | return [ |
132 | [ 'lat' => floatval( $lat ), 'lon' => floatval( $lon ), 'globe' => $globe->getName() ], |
133 | $radius, |
134 | ]; |
135 | } |
136 | |
137 | /** |
138 | * @param string $distance |
139 | * @return int|null Parsed distance in meters, or null if unparsable |
140 | */ |
141 | public static function parseDistance( $distance ) { |
142 | if ( !preg_match( '/^(\d+)(m|km|mi|ft|yd)$/', $distance, $matches ) ) { |
143 | return null; |
144 | } |
145 | |
146 | $scale = [ |
147 | 'm' => 1, |
148 | 'km' => 1000, |
149 | // Supported non-SI units, and their conversions, sourced from |
150 | // https://en.wikipedia.org/wiki/Unit_of_length#Imperial.2FUS |
151 | 'mi' => 1609.344, |
152 | 'ft' => 0.3048, |
153 | 'yd' => 0.9144, |
154 | ]; |
155 | |
156 | return max( 10, (int)round( (int)$matches[1] * $scale[$matches[2]] ) ); |
157 | } |
158 | } |