Passed
Push — rm-common ( 88d3f9 )
by Jeroen De
02:57
created

LatLongParserBase::resolveDirection()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 31
ccs 0
cts 16
cp 0
rs 8.439
cc 5
eloc 16
nc 4
nop 1
crap 30
1
<?php
2
3
namespace DataValues\Geo\Parsers;
4
5
use DataValues\Geo\Values\LatLongValue;
6
use ValueParsers\ParseException;
7
use ValueParsers\ParserOptions;
8
use ValueParsers\StringValueParser;
9
use ValueParsers\ValueParser;
10
11
/**
12
 * @since 0.1, renamed in 2.0
13
 *
14
 * @license GPL-2.0+
15
 * @author H. Snater < [email protected] >
16
 * @author Jeroen De Dauw < [email protected] >
17
 */
18
abstract class LatLongParserBase implements ValueParser {
19
20
	const FORMAT_NAME = 'geo-coordinate';
21
22
	/**
23
	 * The symbols representing the different directions for usage in directional notation.
24
	 */
25
	const OPT_NORTH_SYMBOL = 'north';
26
	const OPT_EAST_SYMBOL = 'east';
27
	const OPT_SOUTH_SYMBOL = 'south';
28
	const OPT_WEST_SYMBOL = 'west';
29
30
	/**
31
	 * The symbol to use as separator between latitude and longitude.
32
	 */
33
	const OPT_SEPARATOR_SYMBOL = 'separator';
34
35
	/**
36
	 * Delimiters used to split a coordinate string when unable to split by using the separator.
37
	 * @var string[]
38
	 */
39
	protected $defaultDelimiters;
40
41
	/**
42
	 * @var ParserOptions
43
	 */
44
	protected $options;
45
46
	public function __construct( ParserOptions $options = null ) {
47
		$this->options = $options ?: new ParserOptions();
48
49
		$this->options->defaultOption( ValueParser::OPT_LANG, 'en' );
50
51
		$this->options->defaultOption( self::OPT_NORTH_SYMBOL, 'N' );
52
		$this->options->defaultOption( self::OPT_EAST_SYMBOL, 'E' );
53
		$this->options->defaultOption( self::OPT_SOUTH_SYMBOL, 'S' );
54
		$this->options->defaultOption( self::OPT_WEST_SYMBOL, 'W' );
55
56
		$this->options->defaultOption( self::OPT_SEPARATOR_SYMBOL, ',' );
57
	}
58
59
	/**
60
	 * Parses a single coordinate segment (either latitude or longitude) and returns it as a float.
61
	 *
62
	 * @param string $coordinateSegment
63
	 *
64
	 * @throws ParseException
65
	 * @return float
66
	 */
67
	abstract protected function getParsedCoordinate( $coordinateSegment );
68
69
	/**
70
	 * Returns whether a coordinate split into its two segments is in the representation expected by
71
	 * this parser.
72
	 *
73
	 * @param string[] $normalizedCoordinateSegments
74
	 *
75
	 * @return boolean
76
	 */
77
	abstract protected function areValidCoordinates( array $normalizedCoordinateSegments );
78
79
	/**
80
	 * @see ValueParser::parse
81
	 *
82
	 * @param string $value
83
	 *
84
	 * @throws ParseException
85
	 * @return LatLongValue
86
	 */
87
	public function parse( $value ) {
88
		$rawValue = $value;
89
90
		$value = $this->removeInvalidChars( $value );
91
92
		$normalizedCoordinateSegments = $this->splitString( $value );
93
94
		if ( !$this->areValidCoordinates( $normalizedCoordinateSegments ) ) {
95
			throw new ParseException( 'Not a valid geographical coordinate', $rawValue, static::FORMAT_NAME );
96
		}
97
98
		list( $latitude, $longitude ) = $normalizedCoordinateSegments;
99
100
		return new LatLongValue(
101
			$this->getParsedCoordinate( $latitude ),
102
			$this->getParsedCoordinate( $longitude )
103
		);
104
	}
105
106
	/**
107
	 * Returns a string trimmed and with control characters and characters with ASCII values above
108
	 * 126 removed. SPACE characters within the string are not removed to retain the option to split
109
	 * the string using that character.
110
	 *
111
	 * @param string $string
112
	 *
113
	 * @return string
114
	 */
115
	protected function removeInvalidChars( $string ) {
116
		$filtered = [];
117
118
		foreach ( str_split( $string ) as $character ) {
119
			$asciiValue = ord( $character );
120
121
			if (
122
				( $asciiValue >= 32 && $asciiValue < 127 )
123
				|| $asciiValue == 194
124
				|| $asciiValue == 176
125
			) {
126
				$filtered[] = $character;
127
			}
128
		}
129
130
		return trim( implode( '', $filtered ) );
131
	}
132
133
	/**
134
	 * Splits a string into two strings using the separator specified in the options. If the string
135
	 * could not be split using the separator, the method will try to split the string by analyzing
136
	 * the used symbols. If the string could not be split into two parts, an empty array is
137
	 * returned.
138
	 *
139
	 * @param string $normalizedCoordinateString
140
	 *
141
	 * @throws ParseException if unable to split input string into two segments
142
	 * @return string[]
143
	 */
144
	protected function splitString( $normalizedCoordinateString ) {
145
		$separator = $this->getOption( self::OPT_SEPARATOR_SYMBOL );
146
147
		$normalizedCoordinateSegments = explode( $separator, $normalizedCoordinateString );
148
149
		if ( count( $normalizedCoordinateSegments ) !== 2 ) {
150
			// Separator not present within the string, trying to figure out the segments by
151
			// splitting after the first direction character or degree symbol:
152
			$delimiters = $this->defaultDelimiters;
153
154
			$ns = [
155
				$this->getOption( self::OPT_NORTH_SYMBOL ),
156
				$this->getOption( self::OPT_SOUTH_SYMBOL )
157
			];
158
159
			$ew = [
160
				$this->getOption( self::OPT_EAST_SYMBOL ),
161
				$this->getOption( self::OPT_WEST_SYMBOL )
162
			];
163
164
			foreach ( $ns as $delimiter ) {
165
				if ( mb_strpos( $normalizedCoordinateString, $delimiter ) === 0 ) {
166
					// String starts with "north" or "west" symbol: Separation needs to be done
167
					// before the "east" or "west" symbol.
168
					$delimiters = array_merge( $ew, $delimiters );
169
					break;
170
				}
171
			}
172
173
			if ( count( $delimiters ) !== count( $this->defaultDelimiters ) + 2 ) {
174
				$delimiters = array_merge( $ns, $delimiters );
175
			}
176
177
			foreach ( $delimiters as $delimiter ) {
178
				$delimiterPos = mb_strpos( $normalizedCoordinateString, $delimiter );
179
				if ( $delimiterPos !== false ) {
180
					$adjustPos = ( in_array( $delimiter, $ew ) ) ? 0 : mb_strlen( $delimiter );
181
					$normalizedCoordinateSegments = [
182
						mb_substr( $normalizedCoordinateString, 0, $delimiterPos + $adjustPos ),
183
						mb_substr( $normalizedCoordinateString, $delimiterPos + $adjustPos )
184
					];
185
					break;
186
				}
187
			}
188
		}
189
190
		if ( count( $normalizedCoordinateSegments ) !== 2 ) {
191
			throw new ParseException( __CLASS__ . ': Unable to split string '
192
				. $normalizedCoordinateString . ' into two coordinate segments' );
193
		}
194
195
		return $normalizedCoordinateSegments;
196
	}
197
198
	/**
199
	 * Turns directional notation (N/E/S/W) of a single coordinate into non-directional notation
200
	 * (+/-).
201
	 * This method assumes there are no preceding or tailing spaces.
202
	 *
203
	 * @param string $coordinateSegment
204
	 *
205
	 * @return string
206
	 */
207
	protected function resolveDirection( $coordinateSegment ) {
208
		$n = $this->getOption( self::OPT_NORTH_SYMBOL );
209
		$e = $this->getOption( self::OPT_EAST_SYMBOL );
210
		$s = $this->getOption( self::OPT_SOUTH_SYMBOL );
211
		$w = $this->getOption( self::OPT_WEST_SYMBOL );
212
213
		// If there is a direction indicator, remove it, and prepend a minus sign for south and west
214
		// directions. If there is no direction indicator, the coordinate is already non-directional
215
		// and no work is required.
216
		foreach ( [ $n, $e, $s, $w ] as $direction ) {
217
			// The coordinate segment may either start or end with a direction symbol.
218
			preg_match(
219
				'/^(' . $direction . '|)([^' . $direction . ']+)(' . $direction . '|)$/i',
220
				$coordinateSegment,
221
				$matches
222
			);
223
224
			if ( $matches[1] === $direction || $matches[3] === $direction ) {
225
				$coordinateSegment = $matches[2];
226
227
				if ( in_array( $direction, [ $s, $w ] ) ) {
228
					$coordinateSegment = '-' . $coordinateSegment;
229
				}
230
231
				return $coordinateSegment;
232
			}
233
		}
234
235
		// Coordinate segment does not include a direction symbol.
236
		return $coordinateSegment;
237
	}
238
239
	protected function getOption( $optionName ) {
240
		return $this->options->getOption( $optionName );
241
	}
242
243
}
244