Completed
Push — milestone/4.0 ( 4a7219...1803cd )
by Marcus
01:48
created

parseDecimalMinutesWithCardinalLetters()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
rs 8.9297
c 0
b 0
f 0
cc 6
nc 5
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Phpgeo\Factory;
6
7
use InvalidArgumentException;
8
use Phpgeo\Point;
9
use Phpgeo\Ellipsoid;
10
11
/**
12
 * Point Factory
13
 *
14
 * @author Marcus Jaschen <[email protected]>
15
 */
16
class PointFactory implements GeometryFactoryInterface
17
{
18
    /**
19
     * Creates a Point instance from the given string.
20
     *
21
     * The string is parsed by a regular expression for a known
22
     * format of geographical coordinates.
23
     *
24
     * @param string $string formatted geographical coordinates
25
     * @param Ellipsoid $ellipsoid
26
     *
27
     * @return Point
28
     * @throws InvalidArgumentException
29
     */
30
    public static function fromString(string $string, Ellipsoid $ellipsoid = null): Point
31
    {
32
        $string = self::mergeSecondsToMinutes($string);
33
34
        $result = self::parseDecimalMinutesWithoutCardinalLetters($string, $ellipsoid);
35
36
        if ($result instanceof Point) {
37
            return $result;
38
        }
39
40
        $result = self::parseDecimalMinutesWithCardinalLetters($string, $ellipsoid);
41
42
        if ($result instanceof Point) {
43
            return $result;
44
        }
45
46
        $result = self::parseDecimalDegreesWithoutCardinalLetters($string, $ellipsoid);
47
48
        if ($result instanceof Point) {
49
            return $result;
50
        }
51
52
        $result = self::parseDecimalDegreesWithCardinalLetters($string, $ellipsoid);
53
54
        if ($result instanceof Point) {
55
            return $result;
56
        }
57
58
        throw new InvalidArgumentException('Format of coordinates was not recognized');
59
    }
60
61
    /**
62
     * @param string $string
63
     * @param Ellipsoid $ellipsoid
64
     *
65
     * @return Point|null
66
     * @throws InvalidArgumentException
67
     */
68
    private static function parseDecimalMinutesWithoutCardinalLetters(
69
        string $string,
70
        Ellipsoid $ellipsoid = null
71
    ): ?Point {
72
        // Decimal minutes without cardinal letters, e. g. "52 12.345, 13 23.456",
73
        // "52° 12.345, 13° 23.456", "52° 12.345′, 13° 23.456′", "52 12.345 N, 13 23.456 E",
74
        // "N52° 12.345′ E13° 23.456′"
75
        $regexp = '/(-?\d{1,2})°?\s+(\d{1,2}\.?\d*)[\'′]?[, ]\s*(-?\d{1,3})°?\s+(\d{1,2}\.?\d*)[\'′]?/u';
76
77
        if (preg_match($regexp, $string, $match) === 1) {
78
            $latitude = (int)$match[1] >= 0
79
                ? (int)$match[1] + (float)$match[2] / 60
80
                : (int)$match[1] - (float)$match[2] / 60;
81
            $longitude = (int)$match[3] >= 0
82
                ? (int)$match[3] + (float)$match[4] / 60
83
                : (int)$match[3] - (float)$match[4] / 60;
84
85
            return new Point((float)$latitude, (float)$longitude, $ellipsoid);
86
        }
87
88
        return null;
89
    }
90
91
    /**
92
     * @param string $string
93
     * @param Ellipsoid $ellipsoid
94
     *
95
     * @return Point|null
96
     * @throws InvalidArgumentException
97
     */
98
    private static function parseDecimalMinutesWithCardinalLetters(string $string, Ellipsoid $ellipsoid = null): ?Point
99
    {
100
        // Decimal minutes with cardinal letters, e. g. "52 12.345, 13 23.456",
101
        // "52° 12.345, 13° 23.456", "52° 12.345′, 13° 23.456′", "52 12.345 N, 13 23.456 E",
102
        // "N52° 12.345′ E13° 23.456′"
103
        $regexp = '/([NS]?\s*)(\d{1,2})°?\s+(\d{1,2}\.?\d*)[\'′]?(\s*[NS]?)';
104
        $regexp .= '[, ]\s*([EW]?\s*)(\d{1,3})°?\s+(\d{1,2}\.?\d*)[\'′]?(\s*[EW]?)/ui';
105
106
        if (preg_match($regexp, $string, $match) === 1) {
107
            $latitude = (int)$match[2] + (float)$match[3] / 60;
108
            if (strtoupper(trim($match[1])) === 'S' || strtoupper(trim($match[4])) === 'S') {
109
                $latitude = -$latitude;
110
            }
111
            $longitude = (int)$match[6] + (float)$match[7] / 60;
112
            if (strtoupper(trim($match[5])) === 'W' || strtoupper(trim($match[8])) === 'W') {
113
                $longitude = -$longitude;
114
            }
115
116
            return new Point((float)$latitude, (float)$longitude, $ellipsoid);
117
        }
118
119
        return null;
120
    }
121
122
    /**
123
     * @param string $string
124
     * @param Ellipsoid $ellipsoid
125
     *
126
     * @return Point|null
127
     * @throws InvalidArgumentException
128
     */
129
    private static function parseDecimalDegreesWithoutCardinalLetters(
130
        string $string,
131
        Ellipsoid $ellipsoid = null
132
    ): ?Point {
133
        // The most simple format: decimal degrees without cardinal letters,
134
        // e. g. "52.5, 13.5" or "53.25732 14.24984"
135
        if (preg_match('/(-?\d{1,2}\.?\d*)°?[, ]\s*(-?\d{1,3}\.?\d*)°?/u', $string, $match) === 1) {
136
            return new Point((float)$match[1], (float)$match[2], $ellipsoid);
137
        }
138
139
        return null;
140
    }
141
142
    /**
143
     * @param string $string
144
     * @param Ellipsoid $ellipsoid
145
     *
146
     * @return Point|null
147
     * @throws InvalidArgumentException
148
     */
149
    private static function parseDecimalDegreesWithCardinalLetters(string $string, Ellipsoid $ellipsoid = null): ?Point
150
    {
151
        // Decimal degrees with cardinal letters, e. g. "N52.5, E13.5",
152
        // "40.2S, 135.3485W", or "56.234°N, 157.245°W"
153
        $regexp = '/([NS]?\s*)(\d{1,2}\.?\d*)°?(\s*[NS]?)[, ]\s*([EW]?\s*)(\d{1,3}\.?\d*)°?(\s*[EW]?)/ui';
154
155
        if (preg_match($regexp, $string, $match) === 1) {
156
            $latitude = $match[2];
157
            if (strtoupper(trim($match[1])) === 'S' || strtoupper(trim($match[3])) === 'S') {
158
                $latitude = -$latitude;
159
            }
160
            $longitude = $match[5];
161
            if (strtoupper(trim($match[4])) === 'W' || strtoupper(trim($match[6])) === 'W') {
162
                $longitude = -$longitude;
163
            }
164
165
            return new Point((float)$latitude, (float)$longitude, $ellipsoid);
166
        }
167
168
        return null;
169
    }
170
171
    /**
172
     * @param string $string
173
     *
174
     * @return string
175
     */
176
    private static function mergeSecondsToMinutes(string $string): string
177
    {
178
        return preg_replace_callback(
179
            '/(\d+)(°|\s)\s*(\d+)(\'|′|\s)(\s*([0-9\.]*))("|\'\'|″|′′)?/u',
180
            function (array $matches): string {
181
                return sprintf('%d %f', $matches[1], $matches[3] + $matches[6] / 60);
182
            },
183
            $string
184
        );
185
    }
186
}
187