Passed
Push — master ( 6e37f8...05a985 )
by Jeroen De
02:43
created

DdCoordinateParser::parse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace DataValues\Geo\Parsers;
4
5
use DataValues\Geo\Values\LatLongValue;
6
use ValueParsers\ParseException;
7
use ValueParsers\ParserOptions;
8
9
/**
10
 * Parser for geographical coordinates in Decimal Degree notation.
11
 *
12
 * @since 0.1
13
 *
14
 * @license GPL-2.0+
15
 * @author Jeroen De Dauw < [email protected] >
16
 * @author H. Snater < [email protected] >
17
 */
18
class DdCoordinateParser extends LatLongParserBase {
19
20
	/**
21
	 * The symbol representing degrees.
22
	 * @since 0.1
23
	 */
24
	const OPT_DEGREE_SYMBOL = 'degree';
25
26
	/**
27
	 * @param ParserOptions|null $options
28
	 */
29 21
	public function __construct( ParserOptions $options = null ) {
30 21
		parent::__construct( $options );
31
32 21
		$this->options->defaultOption( self::OPT_DEGREE_SYMBOL, '°' );
33
34 21
		$this->defaultDelimiters = [ $this->getOption( self::OPT_DEGREE_SYMBOL ) ];
0 ignored issues
show
Documentation Bug introduced by jeroendedauw
It seems like array($this->getOption(self::OPT_DEGREE_SYMBOL)) of type array<integer,*,{"0":"*"}> is incompatible with the declared type array<integer,string> of property $defaultDelimiters.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
35 21
	}
36
37
	/**
38
	 * @see LatLongParserBase::getParsedCoordinate
39
	 *
40
	 * @param string $coordinateSegment
41
	 *
42
	 * @return float
43
	 */
44 16
	protected function getParsedCoordinate( $coordinateSegment ) {
45 16
		$coordinateSegment = $this->resolveDirection( $coordinateSegment );
46 16
		return $this->parseCoordinate( $coordinateSegment );
47
	}
48
49
	/**
50
	 * @see LatLongParserBase::areValidCoordinates
51
	 *
52
	 * @param string[] $normalizedCoordinateSegments
53
	 *
54
	 * @return bool
55
	 */
56 21
	protected function areValidCoordinates( array $normalizedCoordinateSegments ) {
57
		// TODO: Implement localized decimal separator.
58 21
		$baseRegExp = '\d{1,3}(\.\d{1,20})?' . $this->getOption( self::OPT_DEGREE_SYMBOL );
59
60
		// Cache whether the coordinates are specified in directional format (a mixture of
61
		// directional and non-directional is regarded invalid).
62 21
		$directional = false;
63
64 21
		$match = false;
65
66 21
		foreach ( $normalizedCoordinateSegments as $i => $segment ) {
67
			$direction = '('
68 21
				. $this->getOption( self::OPT_NORTH_SYMBOL ) . '|'
69 21
				. $this->getOption( self::OPT_SOUTH_SYMBOL ) . ')';
70
71 21
			if ( $i === 1 ) {
72
				$direction = '('
73 16
					. $this->getOption( self::OPT_EAST_SYMBOL ) . '|'
74 16
					. $this->getOption( self::OPT_WEST_SYMBOL ) . ')';
75
			}
76
77 21
			$match = preg_match(
78 21
				'/^(' . $baseRegExp . $direction . '|' . $direction . $baseRegExp . ')$/i',
79 21
				$segment
80
			);
81
82 21
			if ( $directional ) {
83
				// Directionality is only set after parsing latitude: When the latitude is
84
				// is directional, the longitude needs to be as well. Therefore we break here since
85
				// checking for directionality is the only check needed for longitude.
86 10
				break;
87 21
			} elseif ( $match ) {
88
				// Latitude is directional, no need to check for non-directionality.
89 10
				$directional = true;
90 10
				continue;
91
			}
92
93 11
			$match = preg_match( '/^(-)?' . $baseRegExp . '$/i', $segment );
94
95 11
			if ( !$match ) {
96
				// Does neither match directional nor non-directional.
97 11
				break;
98
			}
99
		}
100
101 21
		return ( 1 === $match );
102
	}
103
104
	/**
105
	 * @see ValueParser::parse
106
	 *
107
	 * @param string $value
108
	 *
109
	 * @throws ParseException
110
	 * @return LatLongValue
111
	 */
112 21
	public function parse( $value ) {
113 21
		return parent::parse( $this->getNormalizedNotation( $value ) );
114
	}
115
116
	/**
117
	 * Returns a normalized version of the coordinate string.
118
	 *
119
	 * @param string $coordinates
120
	 *
121
	 * @return string
122
	 */
123 21
	protected function getNormalizedNotation( $coordinates ) {
124 21
		$coordinates = str_replace(
125 21
			[ '&#176;', '&deg;' ],
126 21
			$this->getOption( self::OPT_DEGREE_SYMBOL ), $coordinates
127
		);
128
129 21
		$coordinates = $this->removeInvalidChars( $coordinates );
130
131 21
		return $coordinates;
132
	}
133
134
	/**
135
	 * Returns a string with whitespace, control characters and characters with ASCII values above
136
	 * 126 removed.
137
	 *
138
	 * @see LatLongParserBase::removeInvalidChars
139
	 *
140
	 * @param string $string
141
	 *
142
	 * @return string
143
	 */
144 21
	protected function removeInvalidChars( $string ) {
145 21
		return str_replace( ' ', '', parent::removeInvalidChars( $string ) );
146
	}
147
148
	/**
149
	 * Converts a coordinate segment to float representation.
150
	 *
151
	 * @param string $coordinateSegment
152
	 *
153
	 * @return float
154
	 */
155 16
	protected function parseCoordinate( $coordinateSegment ) {
156 16
		return (float)str_replace(
157 16
			$this->getOption( self::OPT_DEGREE_SYMBOL ),
158 16
			'',
159 16
			$coordinateSegment
160
		);
161
	}
162
163
	/**
164
	 * @see LatLongParserBase::splitString
165
	 *
166
	 * @param string $normalizedCoordinateString
167
	 *
168
	 * @return string[]
169
	 */
170 21
	protected function splitString( $normalizedCoordinateString ) {
171 21
		$separator = $this->getOption( self::OPT_SEPARATOR_SYMBOL );
172
173 21
		$normalizedCoordinateSegments = explode( $separator, $normalizedCoordinateString );
174
175 21
		if ( count( $normalizedCoordinateSegments ) !== 2 ) {
176
			// Separator not present within the string, trying to figure out the segments by
177
			// splitting after the first direction character or degree symbol:
178 15
			$delimiters = $this->defaultDelimiters;
179
180
			$ns = [
181 15
				$this->getOption( self::OPT_NORTH_SYMBOL ),
182 15
				$this->getOption( self::OPT_SOUTH_SYMBOL )
183
			];
184
185
			$ew = [
186 15
				$this->getOption( self::OPT_EAST_SYMBOL ),
187 15
				$this->getOption( self::OPT_WEST_SYMBOL )
188
			];
189
190 15
			foreach ( $ns as $delimiter ) {
191 15
				if ( mb_strpos( $normalizedCoordinateString, $delimiter ) === 0 ) {
192
					// String starts with "north" or "west" symbol: Separation needs to be done
193
					// before the "east" or "west" symbol.
194 2
					$delimiters = array_merge( $ew, $delimiters );
195 15
					break;
196
				}
197
			}
198
199 15
			if ( count( $delimiters ) !== count( $this->defaultDelimiters ) + 2 ) {
200 13
				$delimiters = array_merge( $ns, $delimiters );
201
			}
202
203 15
			foreach ( $delimiters as $delimiter ) {
204 15
				$delimiterPos = mb_strpos( $normalizedCoordinateString, $delimiter );
205 15
				if ( $delimiterPos !== false ) {
206 10
					$adjustPos = ( in_array( $delimiter, $ew ) ) ? 0 : mb_strlen( $delimiter );
207
					$normalizedCoordinateSegments = [
208 10
						mb_substr( $normalizedCoordinateString, 0, $delimiterPos + $adjustPos ),
209 10
						mb_substr( $normalizedCoordinateString, $delimiterPos + $adjustPos )
210
					];
211 15
					break;
212
				}
213
			}
214
		}
215
216 21
		return $normalizedCoordinateSegments;
217
	}
218
219
}
220