1 | <?php |
||
2 | |||
3 | namespace JosKolenberg\Cardinal; |
||
4 | |||
5 | /** |
||
6 | * Class Cardinal |
||
7 | * |
||
8 | * @property-read float degrees |
||
9 | */ |
||
10 | class Cardinal |
||
11 | { |
||
12 | /** |
||
13 | * @var float |
||
14 | */ |
||
15 | protected $degrees = 0; |
||
16 | |||
17 | /** |
||
18 | * Cardinal constructor. |
||
19 | * |
||
20 | * @param $direction |
||
21 | */ |
||
22 | 48 | public function __construct($direction) |
|
23 | { |
||
24 | 48 | if(is_numeric($direction)){ |
|
25 | 32 | $this->degrees = (float) $direction; |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
26 | |||
27 | 32 | return; |
|
28 | } |
||
29 | |||
30 | 16 | $this->degrees = (float) $this->convertStringToDegrees($direction); |
|
31 | 16 | } |
|
32 | |||
33 | /** |
||
34 | * Make a new Cardinal instance. |
||
35 | * |
||
36 | * @param $direction |
||
37 | * @return \JosKolenberg\Cardinal\Cardinal |
||
38 | */ |
||
39 | 48 | public static function make($direction): self |
|
40 | { |
||
41 | 48 | return new static($direction); |
|
42 | } |
||
43 | |||
44 | /** |
||
45 | * Format the Cardinal into e.g. 'N', 'NE', 'SSW', etc. |
||
46 | * |
||
47 | * Use the $full and $divider parameter to convert |
||
48 | * to fully written directions. E.g. 'NORTH-WEST'. |
||
49 | * |
||
50 | * @param int $precision |
||
51 | * @param bool $full |
||
52 | * @param string $divider |
||
53 | * @return string |
||
54 | */ |
||
55 | 24 | public function format(int $precision = 2, bool $full = false, string $divider = ''): string |
|
56 | { |
||
57 | 24 | if($full){ |
|
58 | 12 | return implode($divider, $this->arrayToFull($this->formatToArray($precision))); |
|
59 | } |
||
60 | |||
61 | 12 | return implode($divider, $this->formatToArray($precision)); |
|
62 | } |
||
63 | |||
64 | /** |
||
65 | * Localized counterpart of format(). |
||
66 | * |
||
67 | * Override lang() method to adjust to localization. |
||
68 | * |
||
69 | * @param int $precision |
||
70 | * @param bool $full |
||
71 | * @param string $divider |
||
72 | * @return string |
||
73 | */ |
||
74 | 8 | public function formatLocalized(int $precision = 2, bool $full = false, string $divider = ''): string |
|
75 | { |
||
76 | 8 | if($full){ |
|
77 | 8 | return implode($divider, $this->translateArray($this->arrayToFull($this->formatToArray($precision)))); |
|
78 | } |
||
79 | |||
80 | 4 | return implode($divider, $this->translateArray($this->formatToArray($precision))); |
|
81 | } |
||
82 | |||
83 | /** |
||
84 | * Get the number of degrees. |
||
85 | * |
||
86 | * @return float |
||
87 | */ |
||
88 | 12 | public function degrees(): float |
|
89 | { |
||
90 | 12 | return $this->degrees; |
|
91 | } |
||
92 | |||
93 | /** |
||
94 | * Convert to a string. |
||
95 | * |
||
96 | * @return string |
||
97 | */ |
||
98 | 4 | public function __toString(): string |
|
99 | { |
||
100 | 4 | return $this->format(2, true, '-'); |
|
101 | } |
||
102 | |||
103 | /** |
||
104 | * Make degrees property read-only available. |
||
105 | * |
||
106 | * @param $name |
||
107 | * @return mixed |
||
108 | */ |
||
109 | 4 | public function __get($name) |
|
110 | { |
||
111 | 4 | if($name === 'degrees'){ |
|
112 | 4 | return $this->degrees; |
|
113 | } |
||
114 | } |
||
115 | |||
116 | /** |
||
117 | * Define the language array. |
||
118 | * Override to adjust to your own language. |
||
119 | * |
||
120 | * @return array |
||
121 | */ |
||
122 | 4 | protected function lang(): array |
|
123 | { |
||
124 | return [ |
||
125 | 4 | 'N' => 'N', |
|
126 | 'E' => 'E', |
||
127 | 'S' => 'S', |
||
128 | 'W' => 'W', |
||
129 | 'NORTH' => 'North', |
||
130 | 'EAST' => 'East', |
||
131 | 'SOUTH' => 'South', |
||
132 | 'WEST' => 'West', |
||
133 | ]; |
||
134 | } |
||
135 | |||
136 | /** |
||
137 | * Format into a single precision array with letter; e.g. ['N'], ['E'], ['S'] or ['W']. |
||
138 | * |
||
139 | * @return array |
||
140 | */ |
||
141 | 20 | protected function formatSingle(): array |
|
142 | { |
||
143 | $values = [ |
||
144 | 20 | ['N'], |
|
145 | ['E'], |
||
146 | ['S'], |
||
147 | ['W'], |
||
148 | ]; |
||
149 | |||
150 | 20 | for ($i = 0; $i < 3; $i++){ |
|
151 | 20 | $min = $this->getRangeMin($i, 360/4); |
|
152 | 20 | $max = $this->getRangeMax($i, 360/4); |
|
153 | |||
154 | 20 | if($this->inRange($min, $max)){ |
|
155 | 20 | break; |
|
156 | } |
||
157 | } |
||
158 | |||
159 | 20 | return $values[$i]; |
|
160 | } |
||
161 | |||
162 | /** |
||
163 | * Format into double precision array of letters; e.g. ['N', 'N','E'], ['E'], ['S','E'], etc. |
||
164 | * |
||
165 | * @return array |
||
166 | */ |
||
167 | 24 | protected function formatDouble(): array |
|
168 | { |
||
169 | $values = [ |
||
170 | 24 | ['N'], |
|
171 | ['N','E'], |
||
172 | ['E'], |
||
173 | ['S','E'], |
||
174 | ['S'], |
||
175 | ['S','W'], |
||
176 | ['W'], |
||
177 | ['N','W'], |
||
178 | ]; |
||
179 | |||
180 | 24 | for ($i = 0; $i < 7; $i++){ |
|
181 | 24 | $min = $this->getRangeMin($i, 360/8); |
|
182 | 24 | $max = $this->getRangeMax($i, 360/8); |
|
183 | |||
184 | 24 | if($this->inRange($min, $max)){ |
|
185 | 20 | break; |
|
186 | } |
||
187 | } |
||
188 | |||
189 | 24 | return $values[$i]; |
|
190 | } |
||
191 | |||
192 | /** |
||
193 | * Format into triple precision array of letters; e.g. ['N'], ['N','N','E'], ['N','E'], ['E','N','E'], etc. |
||
194 | * |
||
195 | * @return array |
||
196 | */ |
||
197 | 20 | protected function formatTriple(): array |
|
198 | { |
||
199 | $values = [ |
||
200 | 20 | ['N'], |
|
201 | ['N','N','E'], |
||
202 | ['N','E'], |
||
203 | ['E','N','E'], |
||
204 | ['E'], |
||
205 | ['E','S','E'], |
||
206 | ['S','E'], |
||
207 | ['S','S','E'], |
||
208 | ['S'], |
||
209 | ['S','S','W'], |
||
210 | ['S','W'], |
||
211 | ['W','S','W'], |
||
212 | ['W'], |
||
213 | ['W','N','W'], |
||
214 | ['N','W'], |
||
215 | ['N','N','W'], |
||
216 | ]; |
||
217 | |||
218 | 20 | for ($i = 0; $i < 15; $i++){ |
|
219 | 20 | $min = $this->getRangeMin($i, 360/16); |
|
220 | 20 | $max = $this->getRangeMax($i, 360/16); |
|
221 | |||
222 | 20 | if($this->inRange($min, $max)){ |
|
223 | 20 | break; |
|
224 | } |
||
225 | } |
||
226 | |||
227 | 20 | return $values[$i]; |
|
228 | } |
||
229 | |||
230 | /** |
||
231 | * Get the number of degrees out of a string. |
||
232 | * E.g. 'E' = 90, 'North-North-East = 22.5, etc. |
||
233 | * |
||
234 | * @param string $direction |
||
235 | * @return float |
||
236 | */ |
||
237 | 16 | protected function convertStringToDegrees(string $direction): float |
|
238 | { |
||
239 | 16 | $direction = strtoupper($direction); |
|
240 | |||
241 | 16 | $sanitizedString = ''; |
|
242 | |||
243 | $search = [ |
||
244 | 16 | 'NORTH' => 'N', |
|
245 | 'EAST' => 'E', |
||
246 | 'SOUTH' => 'S', |
||
247 | 'WEST' => 'W', |
||
248 | 'N' => 'N', |
||
249 | 'E' => 'E', |
||
250 | 'S' => 'S', |
||
251 | 'W' => 'W', |
||
252 | ]; |
||
253 | |||
254 | 16 | while ($direction != ''){ |
|
255 | 16 | $found = false; |
|
256 | |||
257 | 16 | foreach ($search as $string => $letter) { |
|
258 | 16 | if(substr($direction, 0, strlen($string)) === $string){ |
|
259 | 16 | $direction = substr($direction, strlen($string)); |
|
260 | 16 | $sanitizedString .= $letter; |
|
261 | 16 | $found = true; |
|
262 | 16 | break; |
|
263 | } |
||
264 | } |
||
265 | |||
266 | 16 | if(!$found){ |
|
267 | 8 | $direction = substr($direction, 1); |
|
268 | } |
||
269 | } |
||
270 | |||
271 | 16 | return $this->convertSanitizedStringToDegrees($sanitizedString); |
|
272 | } |
||
273 | |||
274 | /** |
||
275 | * Get the number of degrees out of a sanitized string. e.g. 'E' = 90. |
||
276 | * |
||
277 | * @param string $direction |
||
278 | * @return float |
||
279 | */ |
||
280 | 16 | protected function convertSanitizedStringToDegrees(string $direction): float |
|
281 | { |
||
282 | return [ |
||
283 | 'N' => 0, |
||
284 | 'NNE' => 22.5, |
||
285 | 'NE' => 45, |
||
286 | 'ENE' => 67.5, |
||
287 | 'E' => 90, |
||
288 | 'ESE' => 112.5, |
||
289 | 'SE' => 135, |
||
290 | 'SSE' => 157.5, |
||
291 | 'S' => 180, |
||
292 | 'SSW' => 202.5, |
||
293 | 'SW' => 225, |
||
294 | 'WSW' => 247.5, |
||
295 | 'W' => 270, |
||
296 | 'WNW' => 292.5, |
||
297 | 'NW' => 315, |
||
298 | 'NNW' => 337.5, |
||
299 | 16 | ][$direction]; |
|
300 | } |
||
301 | |||
302 | /** |
||
303 | * Get the degree at which a range starts. Given the size of |
||
304 | * a single part of the compass and the index of a range. |
||
305 | * North always being index 0 and counting clock-wise. |
||
306 | * |
||
307 | * @param int $pieceOfPieIndex |
||
308 | * @param float $pieceOfPieSize |
||
309 | * @return float |
||
310 | */ |
||
311 | 32 | protected function getRangeMin(int $pieceOfPieIndex, float $pieceOfPieSize): float |
|
312 | { |
||
313 | 32 | if($pieceOfPieIndex === 0){ |
|
314 | 32 | return 360 - ($pieceOfPieSize / 2); |
|
315 | } |
||
316 | |||
317 | 32 | return ($pieceOfPieIndex * $pieceOfPieSize) - ($pieceOfPieSize / 2); |
|
318 | } |
||
319 | |||
320 | /** |
||
321 | * Max counterpart of getRangeMin. |
||
322 | * |
||
323 | * @param int $pieceOfPieIndex |
||
324 | * @param float $pieceOfPieSize |
||
325 | * @return float |
||
326 | */ |
||
327 | 32 | protected function getRangeMax(int $pieceOfPieIndex, float $pieceOfPieSize): float |
|
328 | { |
||
329 | 32 | return ($pieceOfPieIndex * $pieceOfPieSize) + ($pieceOfPieSize / 2); |
|
330 | } |
||
331 | |||
332 | /** |
||
333 | * Check if the degrees fall into the given min/max range. |
||
334 | * |
||
335 | * @param float $min |
||
336 | * @param float $max |
||
337 | * @return bool |
||
338 | */ |
||
339 | 32 | protected function inRange(float $min, float $max) :bool |
|
340 | { |
||
341 | 32 | if($min > $max){ |
|
342 | 32 | return ($this->degrees >= $min || $this->degrees < $max); |
|
343 | } |
||
344 | |||
345 | 32 | if($this->degrees >= $min && $this->degrees < $max){ |
|
346 | 28 | return true; |
|
347 | } |
||
348 | |||
349 | 32 | return false; |
|
350 | |||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Format the degrees into an array of single letters |
||
355 | * which can be translated into a string. |
||
356 | * |
||
357 | * E.g. 23 degrees with triple precision returns ['N', 'N', 'E'] for 'NNE'. |
||
358 | * |
||
359 | * @param int $precision |
||
360 | * @return array |
||
361 | */ |
||
362 | 32 | protected function formatToArray(int $precision): array |
|
363 | { |
||
364 | switch ($precision){ |
||
365 | 32 | case 1: |
|
366 | 20 | return $this->formatSingle(); |
|
367 | 28 | case 3: |
|
368 | 20 | return $this->formatTriple(); |
|
369 | 24 | case 2: |
|
370 | default: |
||
371 | 24 | return $this->formatDouble(); |
|
372 | } |
||
373 | } |
||
374 | |||
375 | /** |
||
376 | * Translate an array of directions. |
||
377 | * |
||
378 | * E.g. ['N', 'E'] => ['N', 'E'] |
||
379 | * or ['NORTH'] => ['North'] |
||
380 | * |
||
381 | * @param array $directions |
||
382 | * @return array |
||
383 | */ |
||
384 | 8 | protected function translateArray(array $directions): array |
|
385 | { |
||
386 | 8 | return array_map([$this, 'translateSingle'], $directions); |
|
387 | } |
||
388 | |||
389 | /** |
||
390 | * Translate a single direction into a localized string. |
||
391 | * |
||
392 | * @param string $direction |
||
393 | * @return string |
||
394 | */ |
||
395 | 8 | protected function translateSingle(string $direction): string |
|
396 | { |
||
397 | 8 | return $this->lang()[$direction]; |
|
398 | } |
||
399 | |||
400 | /** |
||
401 | * Convert an array with single letter directions |
||
402 | * to fully written directions. |
||
403 | * |
||
404 | * E.g. ['N', 'E'] => ['NORTH', 'EAST'] |
||
405 | * |
||
406 | * @param array $directions |
||
407 | * @return array |
||
408 | */ |
||
409 | 5 | protected function arrayToFull(array $directions): array |
|
410 | { |
||
411 | 15 | return array_map(function ($item){ |
|
412 | return [ |
||
413 | 'N' => 'NORTH', |
||
414 | 'E' => 'EAST', |
||
415 | 'S' => 'SOUTH', |
||
416 | 'W' => 'WEST', |
||
417 | 20 | ][$item]; |
|
418 | 20 | }, $directions); |
|
419 | } |
||
420 | } |