Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
90.80% |
79 / 87 |
|
83.33% |
10 / 12 |
CRAP | |
0.00% |
0 / 1 |
VuFindGeo | |
90.80% |
79 / 87 |
|
83.33% |
10 / 12 |
46.57 | |
0.00% |
0 / 1 |
logError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parseCoverage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
validateNumericCoordinates | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
validateLines | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
validateExtent | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
validateNorthSouth | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
validateEastWest | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
5.15 | |||
validateCoordinateDistance | |
64.71% |
11 / 17 |
|
0.00% |
0 / 1 |
12.56 | |||
validateCoverageCoordinates | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
6 | |||
getAllCoordinatesFromCoverage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getDisplayCoordinatesFromCoverage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getLabelFromCoverage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | /** |
4 | * XSLT importer support methods for geographic indexing. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (c) Demian Katz 2019. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Import_Tools |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/indexing Wiki |
28 | */ |
29 | |
30 | namespace VuFind\XSLT\Import; |
31 | |
32 | use function call_user_func; |
33 | use function count; |
34 | |
35 | /** |
36 | * XSLT importer support methods for geographic indexing. |
37 | * |
38 | * @category VuFind |
39 | * @package Import_Tools |
40 | * @author Demian Katz <demian.katz@villanova.edu> |
41 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
42 | * @link https://vufind.org/wiki/indexing Wiki |
43 | */ |
44 | class VuFindGeo |
45 | { |
46 | /** |
47 | * Method for logging errors (overrideable for testing purposes) |
48 | * |
49 | * @var callable |
50 | */ |
51 | public static $logMethod = 'error_log'; |
52 | |
53 | /** |
54 | * Log an error message |
55 | * |
56 | * @param string $msg Message |
57 | * |
58 | * @return void |
59 | */ |
60 | protected static function logError($msg) |
61 | { |
62 | call_user_func(static::$logMethod, $msg); |
63 | } |
64 | |
65 | /** |
66 | * Parse a dc:coverage string into a key/value array. |
67 | * |
68 | * @param string $coverage Raw dc:coverage string. |
69 | * |
70 | * @return array |
71 | */ |
72 | protected static function parseCoverage($coverage) |
73 | { |
74 | $parts = array_map('trim', explode(';', $coverage)); |
75 | $parsed = []; |
76 | foreach ($parts as $part) { |
77 | $chunks = array_map('trim', explode('=', $part, 2)); |
78 | if (count($chunks) == 2) { |
79 | [$key, $value] = $chunks; |
80 | $parsed[$key] = $value; |
81 | } |
82 | } |
83 | return $parsed; |
84 | } |
85 | |
86 | /** |
87 | * Return true if the coordinate set is complete and numeric. |
88 | * |
89 | * @param array $coords Output of parseCoverage() in need of validation |
90 | * |
91 | * @return bool |
92 | */ |
93 | protected static function validateNumericCoordinates($coords) |
94 | { |
95 | if ( |
96 | !is_numeric($coords['westlimit'] ?? 'NaN') |
97 | || !is_numeric($coords['eastlimit'] ?? 'NaN') |
98 | || !is_numeric($coords['northlimit'] ?? 'NaN') |
99 | || !is_numeric($coords['southlimit'] ?? 'NaN') |
100 | ) { |
101 | static::logError('Missing or non-numeric coordinate value.'); |
102 | return false; |
103 | } |
104 | return true; |
105 | } |
106 | |
107 | /** |
108 | * Check decimal degree coordinates to make sure they do not form a line at the |
109 | * poles. |
110 | * |
111 | * @param array $coords Output of parseCoverage() in need of validation |
112 | * |
113 | * @return bool |
114 | */ |
115 | protected static function validateLines($coords) |
116 | { |
117 | if ( |
118 | $coords['westlimit'] != $coords['eastlimit'] |
119 | && $coords['northlimit'] == $coords['southlimit'] |
120 | && abs($coords['northlimit']) == 90 |
121 | ) { |
122 | static::logError('Coordinates form a line at the pole'); |
123 | return false; |
124 | } |
125 | return true; |
126 | } |
127 | |
128 | /** |
129 | * Check decimal degree coordinates to make sure they are within map extent. |
130 | * |
131 | * @param array $coords Output of parseCoverage() in need of validation |
132 | * |
133 | * @return bool |
134 | */ |
135 | protected static function validateExtent($coords) |
136 | { |
137 | if ( |
138 | abs($coords['northlimit']) > 90 |
139 | || abs($coords['southlimit']) > 90 |
140 | || abs($coords['eastlimit']) > 180 |
141 | || abs($coords['westlimit']) > 180 |
142 | ) { |
143 | static::logError('Coordinates exceed map extent.'); |
144 | return false; |
145 | } |
146 | return true; |
147 | } |
148 | |
149 | /** |
150 | * Check decimal degree coordinates to make sure that north is not less than |
151 | * south. |
152 | * |
153 | * @param array $coords Output of parseCoverage() in need of validation |
154 | * |
155 | * @return bool |
156 | */ |
157 | protected static function validateNorthSouth($coords) |
158 | { |
159 | if ($coords['northlimit'] < $coords['southlimit']) { |
160 | static::logError('North < South.'); |
161 | return false; |
162 | } |
163 | return true; |
164 | } |
165 | |
166 | /** |
167 | * Check decimal degree coordinates to make sure that east is not less than west. |
168 | * |
169 | * @param array $coords Output of parseCoverage() in need of validation |
170 | * |
171 | * @return bool |
172 | */ |
173 | protected static function validateEastWest($coords) |
174 | { |
175 | $east = $coords['eastlimit']; |
176 | $west = $coords['westlimit']; |
177 | if ($east < $west) { |
178 | // Convert to 360 degree grid |
179 | if ($east <= 0) { |
180 | $east += 360; |
181 | } |
182 | if ($west < 0) { |
183 | $west += 360; |
184 | } |
185 | // Check again |
186 | if ($east < $west) { |
187 | static::logError('East < West.'); |
188 | return false; |
189 | } |
190 | } |
191 | return true; |
192 | } |
193 | |
194 | /** |
195 | * Check decimal degree coordinates to make sure they are not too close. |
196 | * Coordinates too close will cause Solr to run out of memory during indexing. |
197 | * |
198 | * @param array $coords Output of parseCoverage() in need of validation |
199 | * |
200 | * @return bool |
201 | */ |
202 | protected static function validateCoordinateDistance($coords) |
203 | { |
204 | $distEW = $coords['eastlimit'] - $coords['westlimit']; |
205 | $distNS = $coords['northlimit'] - $coords['southlimit']; |
206 | if ( |
207 | ($coords['northlimit'] == -90 || $coords['southlimit'] == -90) |
208 | && ($distNS > 0 && $distNS < 0.167) |
209 | ) { |
210 | static::logError( |
211 | 'Coordinates < 0.167 degrees from South Pole. Coordinate Distance: ' |
212 | . round($distNS, 2) |
213 | ); |
214 | return false; |
215 | } |
216 | |
217 | if ( |
218 | ($coords['westlimit'] == 0 || $coords['eastlimit'] == 0) |
219 | && ($distEW > -2 && $distEW < 0) |
220 | ) { |
221 | static::logError( |
222 | 'Coordinates within 2 degrees of Prime Meridian. ' |
223 | . 'Coordinate Distance: ' . round($distEW, 2) |
224 | ); |
225 | return false; |
226 | } |
227 | return true; |
228 | } |
229 | |
230 | /** |
231 | * Return true if the coordinate set is valid for inclusion in VuFind's index. |
232 | * |
233 | * @param array $coords Output of parseCoverage() in need of validation |
234 | * |
235 | * @return bool |
236 | */ |
237 | protected static function validateCoverageCoordinates($coords) |
238 | { |
239 | return static::validateNumericCoordinates($coords) |
240 | && static::validateLines($coords) |
241 | && static::validateExtent($coords) |
242 | && static::validateNorthSouth($coords) |
243 | && static::validateEastWest($coords) |
244 | && static::validateCoordinateDistance($coords); |
245 | } |
246 | |
247 | /** |
248 | * Format valid coordinates for indexing into Solr; return empty string if |
249 | * coordinates are invalid. |
250 | * |
251 | * @param string $coverage Raw dc:coverage string. |
252 | * |
253 | * @return string |
254 | */ |
255 | public static function getAllCoordinatesFromCoverage($coverage) |
256 | { |
257 | $coords = static::parseCoverage($coverage); |
258 | return static::validateCoverageCoordinates($coords) |
259 | ? sprintf( |
260 | 'ENVELOPE(%s,%s,%s,%s)', |
261 | $coords['westlimit'], |
262 | $coords['eastlimit'], |
263 | $coords['northlimit'], |
264 | $coords['southlimit'] |
265 | ) : null; |
266 | } |
267 | |
268 | /** |
269 | * Format valid coordinates for user display; return empty string if |
270 | * coordinates are invalid. |
271 | * |
272 | * @param string $coverage Raw dc:coverage string. |
273 | * |
274 | * @return string |
275 | */ |
276 | public static function getDisplayCoordinatesFromCoverage($coverage) |
277 | { |
278 | $coords = static::parseCoverage($coverage); |
279 | return static::validateCoverageCoordinates($coords) |
280 | ? sprintf( |
281 | '%s %s %s %s', |
282 | $coords['westlimit'], |
283 | $coords['eastlimit'], |
284 | $coords['northlimit'], |
285 | $coords['southlimit'] |
286 | ) : null; |
287 | } |
288 | |
289 | /** |
290 | * Extract a label from a dc:coverage string. |
291 | * |
292 | * @param string $coverage Raw dc:coverage string. |
293 | * |
294 | * @return string |
295 | */ |
296 | public static function getLabelFromCoverage($coverage) |
297 | { |
298 | $coords = static::parseCoverage($coverage); |
299 | return $coords['name'] ?? ''; |
300 | } |
301 | } |