Passed
Push — master ( bbeaaa...9ed6c0 )
by Doug
25:29
created

NTv2Grid   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 100
Duplicated Lines 0 %

Test Coverage

Coverage 88.89%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 59
c 1
b 0
f 0
dl 0
loc 100
ccs 48
cts 54
cp 0.8889
rs 10
wmc 17

4 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getValues() 0 12 1
C determineBestGrid() 0 29 12
A readHeader() 0 29 3
1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord\CoordinateOperation;
10
11
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
12
use SplFileObject;
13
14
use function assert;
15
use function round;
16
use function unpack;
17
use function usort;
18
19
class NTv2Grid extends GeographicGrid
20
{
21
    private const RECORD_SIZE = 16;
22
    private const FLAG_WITHIN_LIMITS = 1;
23
    private const FLAG_ON_UPPER_LATITUDE = 2;
24
    private const FLAG_ON_UPPER_LONGITUDE = 3;
25
    private const FLAG_ON_UPPER_LATITUDE_AND_LONGITUDE = 4;
26
27
    private string $integerFormatChar = 'V';
28
    private string $doubleFormatChar = 'e';
29
    private string $floatFormatChar = 'g';
30
31
    private array $subFileMetaData = [];
32
33 6
    public function __construct($filename)
34
    {
35 6
        $this->gridFile = new SplFileObject($filename);
36 6
        $this->storageOrder = self::STORAGE_ORDER_INCREASING_LATITUDE_INCREASING_LONGITUDE;
37
38 6
        $this->readHeader();
39
    }
40
41
    /**
42
     * @return ArcSecond[]
43
     */
44 7
    public function getValues(float $x, float $y): array
45
    {
46
        // NTv2 is longitude positive *west*
47 7
        $x *= -1;
48
49
        // NTv2 is in seconds, not degrees
50 7
        $x *= 3600;
51 7
        $y *= 3600;
52
53 7
        $gridToUse = $this->determineBestGrid($x, $y);
54
55 7
        return $gridToUse->getValues($x, $y);
56
    }
57
58 6
    private function readHeader(): void
59
    {
60 6
        $this->gridFile->fseek(0);
61 6
        $rawData = $this->gridFile->fread(11 * self::RECORD_SIZE);
62 6
        if (unpack('VNUM_OREC', $rawData, 8)['NUM_OREC'] !== 11) {
63
            $this->integerFormatChar = 'N';
64
            $this->doubleFormatChar = 'E';
65
            $this->floatFormatChar = 'G';
66
        }
67
68 6
        $data = unpack("A8/{$this->integerFormatChar}NUM_OREC/x4/A8/{$this->integerFormatChar}NUM_SREC/x4/A8/{$this->integerFormatChar}NUM_FILE/x4/A8/A8GS_TYPE/A8/A8VERSION/A8/A8SYSTEM_F/A8/A8SYSTEM_T/A8/{$this->doubleFormatChar}MAJOR_F/A8/{$this->doubleFormatChar}MINOR_F/A8/{$this->doubleFormatChar}MAJOR_T/A8/{$this->doubleFormatChar}MINOR_T", $rawData);
69
70 6
        assert($data['GS_TYPE'] === 'SECONDS');
71
72 6
        $subFileStart = 11 * self::RECORD_SIZE;
73 6
        for ($i = 0; $i < $data['NUM_FILE']; ++$i) {
74 6
            $this->gridFile->fseek($subFileStart);
75 6
            $subFileRawData = $this->gridFile->fread(11 * self::RECORD_SIZE);
76 6
            $subFileData = unpack("A8/A8SUB_NAME/A8/A8PARENT/A8/A8CREATED/A8/A8UPDATED/A8/{$this->doubleFormatChar}S_LAT/A8/{$this->doubleFormatChar}N_LAT/A8/{$this->doubleFormatChar}E_LONG/A8/{$this->doubleFormatChar}W_LONG/A8/{$this->doubleFormatChar}LAT_INC/A8/{$this->doubleFormatChar}LONG_INC/A8/{$this->integerFormatChar}GS_COUNT/x4", $subFileRawData);
77 6
            $subFileData['offsetStart'] = $subFileStart;
78
79
            // apply rounding to eliminate fp issues when being deserialized
80 6
            $subFileData['S_LAT'] = round($subFileData['S_LAT'], 5);
81 6
            $subFileData['N_LAT'] = round($subFileData['N_LAT'], 5);
82 6
            $subFileData['E_LONG'] = round($subFileData['E_LONG'], 5);
83 6
            $subFileData['W_LONG'] = round($subFileData['W_LONG'], 5);
84 6
            $this->subFileMetaData[$subFileData['SUB_NAME']] = $subFileData;
85
86 6
            $subFileStart += 11 * self::RECORD_SIZE + $subFileData['GS_COUNT'] * self::RECORD_SIZE;
87
        }
88
    }
89
90 7
    private function determineBestGrid(float $longitude, float $latitude): NTv2SubGrid
91
    {
92 7
        $possibleGrids = [];
93 7
        foreach ($this->subFileMetaData as $subFileMetaDatum) {
94 7
            if ($latitude === $subFileMetaDatum['N_LAT'] && $longitude === $subFileMetaDatum['W_LONG']) {
95
                $possibleGrids[] = [self::FLAG_ON_UPPER_LATITUDE_AND_LONGITUDE, $subFileMetaDatum];
96 7
            } elseif ($longitude === $subFileMetaDatum['W_LONG']) {
97
                $possibleGrids[] = [self::FLAG_ON_UPPER_LONGITUDE, $subFileMetaDatum];
98 7
            } elseif ($latitude === $subFileMetaDatum['N_LAT']) {
99
                $possibleGrids[] = [self::FLAG_ON_UPPER_LATITUDE, $subFileMetaDatum];
100 7
            } elseif ($latitude >= $subFileMetaDatum['S_LAT'] && $latitude <= $subFileMetaDatum['N_LAT'] && $longitude >= $subFileMetaDatum['E_LONG'] && $longitude <= $subFileMetaDatum['W_LONG']) {
101 7
                $possibleGrids[] = [self::FLAG_WITHIN_LIMITS, $subFileMetaDatum];
102
            }
103
        }
104
105 7
        usort($possibleGrids, static fn ($a, $b) => $a[0] <=> $b[0] ?: $a[1]['LAT_INC'] <=> $b[1]['LAT_INC'] ?: $a[2]['LONG_INC'] <=> $b[2]['LONG_INC']);
106
107 7
        $gridToUse = $possibleGrids[0][1];
108
109 7
        return new NTv2SubGrid(
110 7
            $this->gridFile->getPathname(),
111 7
            $gridToUse['offsetStart'],
112 7
            $gridToUse['S_LAT'],
113 7
            $gridToUse['N_LAT'],
114 7
            $gridToUse['E_LONG'],
115 7
            $gridToUse['W_LONG'],
116 7
            $gridToUse['LAT_INC'],
117 7
            $gridToUse['LONG_INC'],
118 7
            $this->floatFormatChar
119
        );
120
    }
121
}
122