NTv2Grid::readHeader()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 3.0303

Importance

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