Passed
Push — master ( 9d3cd0...e94f7b )
by Amir
01:44 queued 11s
created

src/Parsers/DdCoordinateParser.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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