Completed
Push — dropOldAliases ( 17840d...e22dca )
by Jeroen De
12:52 queued 07:28
created

DdCoordinateParser::areValidCoordinates()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 47
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 47
ccs 21
cts 21
cp 1
rs 8.5125
c 0
b 0
f 0
cc 6
eloc 24
nc 7
nop 1
crap 6
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 25
	 */
29 25
	public function __construct( ParserOptions $options = null ) {
30
		parent::__construct( $options );
31 25
32
		$this->options->defaultOption( self::OPT_DEGREE_SYMBOL, '°' );
33 25
34 25
		$this->defaultDelimiters = [ $this->getOption( self::OPT_DEGREE_SYMBOL ) ];
0 ignored issues
show
Documentation Bug introduced by
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
	}
36
37
	/**
38
	 * @see LatLongParserBase::getParsedCoordinate
39
	 *
40
	 * @param string $coordinateSegment
41
	 *
42
	 * @return float
43 16
	 */
44 16
	protected function getParsedCoordinate( $coordinateSegment ) {
45 16
		$coordinateSegment = $this->resolveDirection( $coordinateSegment );
46
		return $this->parseCoordinate( $coordinateSegment );
47
	}
48
49
	/**
50
	 * @see LatLongParserBase::areValidCoordinates
51
	 *
52
	 * @param string[] $normalizedCoordinateSegments
53
	 *
54
	 * @return bool
55 18
	 */
56
	protected function areValidCoordinates( array $normalizedCoordinateSegments ) {
57 18
		// TODO: Implement localized decimal separator.
58
		$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 18
		// directional and non-directional is regarded invalid).
62
		$directional = false;
63 18
64
		$match = false;
65 18
66
		foreach ( $normalizedCoordinateSegments as $i => $segment ) {
67 18
			$direction = '('
68 18
				. $this->getOption( self::OPT_NORTH_SYMBOL ) . '|'
69
				. $this->getOption( self::OPT_SOUTH_SYMBOL ) . ')';
70 18
71
			if ( $i === 1 ) {
72 16
				$direction = '('
73 16
					. $this->getOption( self::OPT_EAST_SYMBOL ) . '|'
74
					. $this->getOption( self::OPT_WEST_SYMBOL ) . ')';
75
			}
76 18
77 18
			$match = preg_match(
78 18
				'/^(' . $baseRegExp . $direction . '|' . $direction . $baseRegExp . ')$/i',
79
				$segment
80
			);
81 18
82
			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 10
				// checking for directionality is the only check needed for longitude.
86 18
				break;
87
			} elseif ( $match ) {
88 10
				// Latitude is directional, no need to check for non-directionality.
89 10
				$directional = true;
90
				continue;
91
			}
92 8
93
			$match = preg_match( '/^(-)?' . $baseRegExp . '$/i', $segment );
94 8
95
			if ( !$match ) {
96 8
				// Does neither match directional nor non-directional.
97
				break;
98
			}
99
		}
100 18
101
		return ( 1 === $match );
102
	}
103
104
	/**
105
	 * @see ValueParser::parse
106
	 *
107
	 * @param string $value
108
	 *
109
	 * @throws ParseException
110 18
	 * @return LatLongValue
111 18
	 */
112
	public function parse( $value ) {
113
		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 18
	 * @return string
122 18
	 */
123 18
	protected function getNormalizedNotation( $coordinates ) {
124 18
		$coordinates = str_replace(
125
			[ '&#176;', '&deg;' ],
126
			$this->getOption( self::OPT_DEGREE_SYMBOL ), $coordinates
127 18
		);
128
129 18
		$coordinates = $this->removeInvalidChars( $coordinates );
130
131
		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 18
	 * @return string
143 18
	 */
144
	protected function removeInvalidChars( $string ) {
145
		return str_replace( ' ', '', parent::removeInvalidChars( $string ) );
146
	}
147
148
	/**
149
	 * Converts a coordinate segment to float representation.
150
	 *
151
	 * @param string $coordinateSegment
152
	 *
153 16
	 * @return float
154 16
	 */
155 16
	protected function parseCoordinate( $coordinateSegment ) {
156 16
		return (float)str_replace(
157 16
			$this->getOption( self::OPT_DEGREE_SYMBOL ),
158
			'',
159
			$coordinateSegment
160
		);
161
	}
162
163
	/**
164
	 * @see LatLongParserBase::splitString
165
	 *
166
	 * @param string $normalizedCoordinateString
167
	 *
168 18
	 * @return string[]
169 18
	 */
170
	protected function splitString( $normalizedCoordinateString ) {
171 18
		$separator = $this->getOption( self::OPT_SEPARATOR_SYMBOL );
172
173 18
		$normalizedCoordinateSegments = explode( $separator, $normalizedCoordinateString );
174
175
		if ( count( $normalizedCoordinateSegments ) !== 2 ) {
176 12
			// Separator not present within the string, trying to figure out the segments by
177
			// splitting after the first direction character or degree symbol:
178
			$delimiters = $this->defaultDelimiters;
179 12
180 12
			$ns = [
181
				$this->getOption( self::OPT_NORTH_SYMBOL ),
182
				$this->getOption( self::OPT_SOUTH_SYMBOL )
183
			];
184 12
185 12
			$ew = [
186
				$this->getOption( self::OPT_EAST_SYMBOL ),
187
				$this->getOption( self::OPT_WEST_SYMBOL )
188 12
			];
189 12
190
			foreach ( $ns as $delimiter ) {
191
				if ( mb_strpos( $normalizedCoordinateString, $delimiter ) === 0 ) {
192 2
					// String starts with "north" or "west" symbol: Separation needs to be done
193 12
					// before the "east" or "west" symbol.
194
					$delimiters = array_merge( $ew, $delimiters );
195
					break;
196
				}
197 12
			}
198 10
199
			if ( count( $delimiters ) !== count( $this->defaultDelimiters ) + 2 ) {
200
				$delimiters = array_merge( $ns, $delimiters );
201 12
			}
202 12
203 12
			foreach ( $delimiters as $delimiter ) {
204 10
				$delimiterPos = mb_strpos( $normalizedCoordinateString, $delimiter );
205
				if ( $delimiterPos !== false ) {
206 10
					$adjustPos = ( in_array( $delimiter, $ew ) ) ? 0 : mb_strlen( $delimiter );
207 10
					$normalizedCoordinateSegments = [
208
						mb_substr( $normalizedCoordinateString, 0, $delimiterPos + $adjustPos ),
209 12
						mb_substr( $normalizedCoordinateString, $delimiterPos + $adjustPos )
210
					];
211
					break;
212
				}
213
			}
214 18
		}
215
216
		return $normalizedCoordinateSegments;
217
	}
218
219
}
220