Passed
Push — master ( b6220f...9d3cd0 )
by Jeroen De
52s queued 11s
created

LatLongParserBase::resolveDirection()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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