Issues (1)

src/Cardinal.php (1 issue)

Labels
Severity
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
The property degrees is declared read-only in JosKolenberg\Cardinal\Cardinal.
Loading history...
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
}