Passed
Push — 4.x ( 2e43a7...18aaa8 )
by Doug
06:37
created

NTv2Grid::determineBestGrid()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 15.2016

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 16
nc 12
nop 2
dl 0
loc 26
ccs 13
cts 17
cp 0.7647
crap 15.2016
rs 6.6166
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord\CoordinateOperation;
10
11
use function abs;
12
use function assert;
13
use InvalidArgumentException;
14
use PHPCoord\CoordinateReferenceSystem\Geographic;
15
use PHPCoord\GeographicPoint;
16
use PHPCoord\UnitOfMeasure\Angle\Angle;
17
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
18
use function round;
19
use SplFileObject;
20
use function unpack;
21
use function usort;
22
23
class NTv2Grid extends SplFileObject
24
{
25
    private const RECORD_SIZE = 16;
26
    private const ITERATION_CONVERGENCE = 0.0001;
27
    private const FLAG_WITHIN_LIMITS = 1;
28
    private const FLAG_ON_UPPER_LATITUDE = 2;
29
    private const FLAG_ON_UPPER_LONGITUDE = 3;
30
    private const FLAG_ON_UPPER_LATITUDE_AND_LONGITUDE = 4;
31
32
    private string $integerFormatChar = 'V';
33
    private string $doubleFormatChar = 'e';
34
    private string $floatFormatChar = 'g';
35
36
    private array $subFileMetaData = [];
37
38
    private int $numberOfOverviewHeaderRecords;
0 ignored issues
show
introduced by
The private property $numberOfOverviewHeaderRecords is not used, and could be removed.
Loading history...
39
    private int $numberOfSubFileHeaderRecords;
0 ignored issues
show
introduced by
The private property $numberOfSubFileHeaderRecords is not used, and could be removed.
Loading history...
40
    private int $numberOfGridShiftSubFiles;
0 ignored issues
show
introduced by
The private property $numberOfGridShiftSubFiles is not used, and could be removed.
Loading history...
41
42
    private float $lowerLatitudeLimit;
0 ignored issues
show
introduced by
The private property $lowerLatitudeLimit is not used, and could be removed.
Loading history...
43
    private float $upperLatitudeLimit;
0 ignored issues
show
introduced by
The private property $upperLatitudeLimit is not used, and could be removed.
Loading history...
44
    private float $lowerLongitudeLimit;
0 ignored issues
show
introduced by
The private property $lowerLongitudeLimit is not used, and could be removed.
Loading history...
45
    private float $upperLongitudeLimit;
0 ignored issues
show
introduced by
The private property $upperLongitudeLimit is not used, and could be removed.
Loading history...
46
    private float $latitudeGridInterval;
0 ignored issues
show
introduced by
The private property $latitudeGridInterval is not used, and could be removed.
Loading history...
47
    private float $longitudeGridInterval;
0 ignored issues
show
introduced by
The private property $longitudeGridInterval is not used, and could be removed.
Loading history...
48
    private string $gridShiftDataType;
0 ignored issues
show
introduced by
The private property $gridShiftDataType is not used, and could be removed.
Loading history...
49
    private string $systemFrom;
0 ignored issues
show
introduced by
The private property $systemFrom is not used, and could be removed.
Loading history...
50
    private string $systemTo;
0 ignored issues
show
introduced by
The private property $systemTo is not used, and could be removed.
Loading history...
51
    private float $semiMinorAxisFrom;
0 ignored issues
show
introduced by
The private property $semiMinorAxisFrom is not used, and could be removed.
Loading history...
52
    private float $semiMinorAxisTo;
0 ignored issues
show
introduced by
The private property $semiMinorAxisTo is not used, and could be removed.
Loading history...
53
54 6
    public function __construct($filename)
55
    {
56 6
        parent::__construct($filename);
57
58 6
        $this->readHeader();
59
    }
60
61 4
    public function applyForwardAdjustment(GeographicPoint $point, Geographic $to): GeographicPoint
62
    {
63 4
        $adjustment = $this->getAdjustment($point->getLatitude(), $point->getLongitude());
64
65 4
        $latitude = $point->getLatitude()->add($adjustment[0]);
66 4
        $longitude = $point->getLongitude()->add($adjustment[1]);
67
68 4
        return GeographicPoint::create($latitude, $longitude, $point->getHeight(), $to, $point->getCoordinateEpoch());
69
    }
70
71 2
    public function applyReverseAdjustment(GeographicPoint $point, Geographic $to): GeographicPoint
72
    {
73 2
        $adjustment = [new ArcSecond(0), new ArcSecond(0)];
74 2
        $latitude = $point->getLatitude();
75 2
        $longitude = $point->getLongitude();
76
77
        do {
78 2
            $prevAdjustment = $adjustment;
79 2
            $adjustment = $this->getAdjustment($latitude, $longitude);
80 2
            $latitude = $point->getLatitude()->subtract($adjustment[0]);
81 2
            $longitude = $point->getLongitude()->subtract($adjustment[1]);
82 2
        } while (abs($adjustment[0]->subtract($prevAdjustment[0])->getValue()) > self::ITERATION_CONVERGENCE && abs($adjustment[1]->subtract($prevAdjustment[1])->getValue()) > self::ITERATION_CONVERGENCE);
83
84 2
        return GeographicPoint::create($latitude, $longitude, $point->getHeight(), $to, $point->getCoordinateEpoch());
85
    }
86
87
    /**
88
     * @return ArcSecond[]
89
     */
90 6
    private function getAdjustment(Angle $latitude, Angle $longitude): array
91
    {
92
        // NTv2 is longitude positive *west*
93 6
        $longitude = $longitude->multiply(-1);
94
95 6
        $latitudeAsSeconds = Angle::convert($latitude, Angle::EPSG_ARC_SECOND)->getValue();
96 6
        $longitudeAsSeconds = Angle::convert($longitude, Angle::EPSG_ARC_SECOND)->getValue();
97 6
        [$flag, $gridToUse] = $this->determineBestGrid($latitudeAsSeconds, $longitudeAsSeconds);
98
99 6
        $rowIndex = (int) (($latitudeAsSeconds - $gridToUse['S_LAT']) / $gridToUse['LAT_INC']);
100 6
        $columnIndex = (int) (($longitudeAsSeconds - $gridToUse['E_LONG']) / $gridToUse['LONG_INC']);
101 6
        $gridPointsPerRow = (int) (($gridToUse['W_LONG'] - $gridToUse['E_LONG']) / $gridToUse['LONG_INC']) + 1;
102 6
        $gridPointsPerColumn = (int) (($gridToUse['N_LAT'] - $gridToUse['S_LAT']) / $gridToUse['LAT_INC']) + 1;
103 6
        $numberOfRecords = $gridPointsPerRow * $gridPointsPerColumn;
104 6
        assert($numberOfRecords === $gridToUse['GS_COUNT']);
105
106 6
        if ($flag === self::FLAG_WITHIN_LIMITS) {
107 6
            $recordIndexLR = $rowIndex * $gridPointsPerRow + $columnIndex;
108 6
            $recordIndexLL = $recordIndexLR + 1;
109 6
            $recordIndexUR = $recordIndexLR + $gridPointsPerRow;
110 6
            $recordIndexUL = $recordIndexUR + 1;
111
        } elseif ($flag === self::FLAG_ON_UPPER_LATITUDE) {
112
            $recordIndexLR = $rowIndex * $gridPointsPerRow + $columnIndex;
113
            $recordIndexLL = $recordIndexLR + 1;
114
            $recordIndexUR = $recordIndexLR;
115
            $recordIndexUL = $recordIndexUR;
116
        } elseif ($flag === self::FLAG_ON_UPPER_LONGITUDE) {
117
            $recordIndexLR = $rowIndex * $gridPointsPerRow + $columnIndex;
118
            $recordIndexUR = $recordIndexLR + $gridPointsPerRow;
119
            $recordIndexLL = $recordIndexLR;
120
            $recordIndexUL = $recordIndexUR;
121
        } elseif ($flag === self::FLAG_ON_UPPER_LATITUDE_AND_LONGITUDE) {
122
            $recordIndexLR = $rowIndex * $gridPointsPerRow + $columnIndex;
123
            $recordIndexLL = $recordIndexLR;
124
            $recordIndexUR = $recordIndexLR;
125
            $recordIndexUL = $recordIndexUR;
126
        }
127
128 6
        $latitudeR = $gridToUse['S_LAT'] + $rowIndex * $gridToUse['LAT_INC'];
129 6
        $longitudeL = $gridToUse['E_LONG'] + $columnIndex * $gridToUse['LONG_INC'];
130
131 6
        $recordLR = $this->getRecord($gridToUse['offsetStart'] + (11 + $recordIndexLR) * self::RECORD_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $recordIndexLR does not seem to be defined for all execution paths leading up to this point.
Loading history...
132 6
        $recordLL = $this->getRecord($gridToUse['offsetStart'] + (11 + $recordIndexLL) * self::RECORD_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $recordIndexLL does not seem to be defined for all execution paths leading up to this point.
Loading history...
133 6
        $recordUR = $this->getRecord($gridToUse['offsetStart'] + (11 + $recordIndexUR) * self::RECORD_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $recordIndexUR does not seem to be defined for all execution paths leading up to this point.
Loading history...
134 6
        $recordUL = $this->getRecord($gridToUse['offsetStart'] + (11 + $recordIndexUL) * self::RECORD_SIZE);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $recordIndexUL does not seem to be defined for all execution paths leading up to this point.
Loading history...
135
136 6
        $x = ($latitudeAsSeconds - $latitudeR) / $gridToUse['LAT_INC'];
137 6
        $y = ($longitudeAsSeconds - $longitudeL) / $gridToUse['LONG_INC'];
138 6
        assert($x >= 0 && $x <= 1);
139 6
        assert($y >= 0 && $y <= 1);
140
141 6
        $eShiftLatitude = $recordLR['LATITUDE_SHIFT'] + ($recordLL['LATITUDE_SHIFT'] - $recordLR['LATITUDE_SHIFT']) * $y;
142 6
        $fShiftLatitude = $recordUR['LATITUDE_SHIFT'] + ($recordUL['LATITUDE_SHIFT'] - $recordUR['LATITUDE_SHIFT']) * $y;
143 6
        $pShiftLatitude = $eShiftLatitude + ($fShiftLatitude - $eShiftLatitude) * $x;
144
145 6
        $eShiftLongitude = $recordLR['LONGITUDE_SHIFT'] + ($recordLL['LONGITUDE_SHIFT'] - $recordLR['LONGITUDE_SHIFT']) * $y;
146 6
        $fShiftLongitude = $recordUR['LONGITUDE_SHIFT'] + ($recordUL['LONGITUDE_SHIFT'] - $recordUR['LONGITUDE_SHIFT']) * $y;
147 6
        $pShiftLongitude = $eShiftLongitude + ($fShiftLongitude - $eShiftLongitude) * $x;
148
149 6
        return [new ArcSecond($pShiftLatitude), new ArcSecond(-$pShiftLongitude)]; // NTv2 is longitude positive *west*
150
    }
151
152 6
    private function getRecord(int $recordIndex): array
153
    {
154 6
        $this->fseek($recordIndex);
155 6
        $rawRecord = $this->fread(self::RECORD_SIZE);
156 6
        $shifts = unpack("{$this->floatFormatChar}LATITUDE_SHIFT/{$this->floatFormatChar}LONGITUDE_SHIFT/{$this->floatFormatChar}LATITUDE_ACCURACY/{$this->floatFormatChar}LONGITUDE_ACCURACY", $rawRecord);
157
158 6
        return $shifts;
159
    }
160
161 6
    private function readHeader(): void
162
    {
163 6
        $this->fseek(0);
164 6
        $rawData = $this->fread(11 * self::RECORD_SIZE);
165 6
        if (unpack('VNUM_OREC', $rawData, 8)['NUM_OREC'] !== 11) {
166
            $this->integerFormatChar = 'N';
167
            $this->doubleFormatChar = 'E';
168
            $this->floatFormatChar = 'G';
169
        }
170
171 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);
172
173 6
        assert($data['GS_TYPE'] === 'SECONDS');
174
175 6
        $subFileStart = 11 * self::RECORD_SIZE;
176 6
        for ($i = 0; $i < $data['NUM_FILE']; ++$i) {
177 6
            $this->fseek($subFileStart);
178 6
            $subFileRawData = $this->fread(11 * self::RECORD_SIZE);
179 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);
180 6
            $subFileData['offsetStart'] = $subFileStart;
181
182
            //apply rounding to eliminate fp issues when being deserialized
183 6
            $subFileData['S_LAT'] = round($subFileData['S_LAT'], 5);
184 6
            $subFileData['N_LAT'] = round($subFileData['N_LAT'], 5);
185 6
            $subFileData['E_LONG'] = round($subFileData['E_LONG'], 5);
186 6
            $subFileData['W_LONG'] = round($subFileData['W_LONG'], 5);
187 6
            $this->subFileMetaData[$subFileData['SUB_NAME']] = $subFileData;
188
189 6
            $subFileStart += 11 * self::RECORD_SIZE + $subFileData['GS_COUNT'] * self::RECORD_SIZE;
190
        }
191
    }
192
193 6
    private function determineBestGrid(float $latitude, float $longitude): array
194
    {
195 6
        $possibleGrids = [];
196 6
        foreach ($this->subFileMetaData as $subFileName => $subFileMetaDatum) {
197 6
            if ($latitude >= $subFileMetaDatum['S_LAT'] && $latitude <= $subFileMetaDatum['N_LAT'] && $longitude >= $subFileMetaDatum['E_LONG'] && $longitude <= $subFileMetaDatum['W_LONG']) {
198 6
                if ($latitude === $subFileMetaDatum['N_LAT'] && $longitude === $subFileMetaDatum['W_LONG']) {
199
                    $possibleGrids[] = [self::FLAG_ON_UPPER_LATITUDE_AND_LONGITUDE, $subFileMetaDatum];
200 6
                } elseif ($longitude === $subFileMetaDatum['W_LONG']) {
201
                    $possibleGrids[] = [self::FLAG_ON_UPPER_LONGITUDE, $subFileMetaDatum];
202 6
                } elseif ($latitude === $subFileMetaDatum['N_LAT']) {
203
                    $possibleGrids[] = [self::FLAG_ON_UPPER_LATITUDE, $subFileMetaDatum];
204
                } else {
205 6
                    $possibleGrids[] = [self::FLAG_WITHIN_LIMITS, $subFileMetaDatum];
206
                }
207
            }
208
        }
209
210 6
        if (!$possibleGrids) {
211
            throw new InvalidArgumentException('Specified coordinates are not within this grid file');
212
        }
213
214 6
        usort($possibleGrids, static function ($a, $b) {
215 4
            return $a[0] <=> $b[0] ?: $a[1]['LAT_INC'] <=> $b[1]['LAT_INC'] ?: $a[2]['LONG_INC'] <=> $b[2]['LONG_INC'];
216 6
        });
217
218 6
        return $possibleGrids[0];
219
    }
220
}
221