Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.80% covered (success)
90.80%
79 / 87
83.33% covered (warning)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
VuFindGeo
90.80% covered (success)
90.80%
79 / 87
83.33% covered (warning)
83.33%
10 / 12
46.57
0.00% covered (danger)
0.00%
0 / 1
 logError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseCoverage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 validateNumericCoordinates
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 validateLines
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 validateExtent
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 validateNorthSouth
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 validateEastWest
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
5.15
 validateCoordinateDistance
64.71% covered (warning)
64.71%
11 / 17
0.00% covered (danger)
0.00%
0 / 1
12.56
 validateCoverageCoordinates
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 getAllCoordinatesFromCoverage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getDisplayCoordinatesFromCoverage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getLabelFromCoverage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
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
30namespace VuFind\XSLT\Import;
31
32use function call_user_func;
33use 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 */
44class 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}