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