Completed
Pull Request — master (#3)
by Hannes
02:16
created

GeoTIFFReader::isLittleEndian()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
/*
4
 * This file is part of the Runalyze DEM Reader.
5
 *
6
 * (c) RUNALYZE <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace Runalyze\DEM\Provider\GeoTIFF;
13
14
use Runalyze\DEM\Exception\RuntimeException;
15
use Runalyze\DEM\Provider\AbstractResourceReader;
16
17
/**
18
 * @see http://www.awaresystems.be/imaging/tiff/tifftags.html
19
 */
20
class GeoTIFFReader extends AbstractResourceReader
21
{
22
    /**
23
     * The number of bytes required to hold a TIFF offset address.
24
     * @var int
25
     */
26
    const LEN_OFFSET = 4;
27
28
    /**
29
     * Magic number located at bytes 2-3 which identifies a TIFF file.
30
     * @var int
31
     */
32
    const MAGIC_TIFF_ID = 42;
33
34
    /** @var int */
35
    const TIFF_CONST_STRIPOFSETS = 273;
36
37
    /** @var int */
38
    const TIFF_CONST_IMAGE_WIDTH = 256;
39
40
    /** @var int */
41
    const TIFF_CONST_IMAGE_LENGTH = 257;
42
43
    /** @var int */
44
    const TIFF_CONST_BITS_PER_SAMPLE = 258;
45
46
    /** @var int */
47
    const TIFF_CONST_SAMPLES_PER_PIXEL = 277;
48
49
    /** @var int */
50
    const TIFF_CONST_ROWS_PER_STRIP = 278;
51
52
    /** @var int */
53
    const TIFF_CONST_STRIPBYTECOUNTS = 279;
54
55
    /** @var int */
56
    const TIFF_CONST_IFD_ENTRY_BYTES = 12;
57
58
    /** @var string */
59
    const BIG_ENDIAN = 'MM';
60
61
    /** @var string */
62
    const LITTLE_ENDIAN = 'II';
63
64
    /** @var int */
65
    const UNKNOWN = -32768;
66
67
    /** @var int The number of bytes containing each item of elevation data */
68
    protected $BytesPerSample = 2;
69
70
    /** @var int 	The number of components per pixel */
71
    protected $SamplesPerPixel = 1;
72
73
    /** @var int The number of rows per strip */
74
    protected $RowsPerStrip = 1;
75
76
    /** @var int */
77
    protected $NumDataRows;
78
79
    /** @var int */
80
    protected $NumDataCols;
81
82
    /** @var int */
83
    protected $StripOffsets;
84
85
    /** @var string */
86
    protected $ByteOrder;
87
88
    public function readHeader()
89
    {
90
        $this->checkByteOrderAndTiffIdentifier();
91
        $this->goToIFDEntries();
92
        $this->readIFDEntries();
93
    }
94
95
    /**
96
     * Go to the file header and work out the byte order (bytes 0-1) and TIFF identifier (bytes 2-3).
97
     * @throws RuntimeException
98
     */
99
    protected function checkByteOrderAndTiffIdentifier()
100
    {
101
        fseek($this->FileResource, 0);
102
        $data = unpack('c2chars/vTIFF_ID', fread($this->FileResource, 4));
103
104
        if (static::MAGIC_TIFF_ID !== $data['TIFF_ID']) {
105
            throw new RuntimeException('Provided GeoTIFF file is not a valid tiff file.');
106
        }
107
108
        $this->ByteOrder = sprintf('%c%c', $data['chars1'], $data['chars2']);
109
110
        if ($this->ByteOrder !== static::LITTLE_ENDIAN && $this->ByteOrder !== static::BIG_ENDIAN) {
111
            throw new RuntimeException('Provided GeoTIFF file has an unknown byte order.');
112
        }
113
    }
114
115
    /**
116
     * The remaining 4 bytes in the header are the offset to the IFD.
117
     */
118
    protected function goToIFDEntries()
119
    {
120
        fseek($this->FileResource, 4);
121
122
        $ifdOffsetFormat = $this->isLittleEndian() ? 'VIFDoffset' : 'NIFDoffset';
123
        $data = unpack($ifdOffsetFormat, fread($this->FileResource, 4));
124
125
        fseek($this->FileResource, $data['IFDoffset']);
126
    }
127
128
    /**
129
     * Read IFD entries which (the number of entries in each is in the first two bytes).
130
     */
131
    protected function readIFDEntries()
132
    {
133
        $countFormat = $this->isLittleEndian() ? 'vcount' : 'ncount';
134
        $countData = unpack($countFormat, fread($this->FileResource, 2));
135
        $ifdFormat = $this->isLittleEndian() ? 'vtag/vtype/Vcount/Voffset' : 'ntag/ntype/Ncount/Noffset';
136
137
        for ($i = 0; $i < $countData['count']; ++$i) {
138
            $constData = unpack($ifdFormat, fread($this->FileResource, static::TIFF_CONST_IFD_ENTRY_BYTES));
139
140
            switch ($constData['tag']) {
141
                case static::TIFF_CONST_IMAGE_WIDTH:
142
                    $this->NumDataCols = $constData['offset'];
143
                    break;
144
                case static::TIFF_CONST_IMAGE_LENGTH:
145
                    $this->NumDataRows = $constData['offset'];
146
                    break;
147
                case static::TIFF_CONST_BITS_PER_SAMPLE:
148
                    $this->BytesPerSample = (int) ($constData['offset'] / 8);
149
                    break;
150
                case static::TIFF_CONST_SAMPLES_PER_PIXEL:
151
                    $this->SamplesPerPixel = $constData['offset'];
152
                    break;
153
                case static::TIFF_CONST_ROWS_PER_STRIP:
154
                    $this->RowsPerStrip = $constData['offset'];
155
                    break;
156
                case static::TIFF_CONST_STRIPOFSETS:
157
                    $this->StripOffsets = $constData['offset'];
158
                    break;
159
            }
160
        }
161
    }
162
163
    /**
164
     * @return bool
165
     */
166
    protected function isLittleEndian()
167
    {
168
        return static::LITTLE_ENDIAN === $this->ByteOrder;
169
    }
170
171
    /**
172
     * @param  float   $relativeLatitude
173
     * @param  float   $relativeLongitude
174
     * @return float[] array(row, col)
175
     */
176
    public function getExactRowAndColFor($relativeLatitude, $relativeLongitude)
177
    {
178
        return [
179
            $relativeLatitude * ($this->NumDataRows - 1),
180
            $relativeLongitude * ($this->NumDataCols - 1),
181
        ];
182
    }
183
184
    /**
185
     * @param  int      $row
186
     * @param  int      $col
187
     * @return int|bool
188
     */
189
    public function getElevationFor($row, $col)
190
    {
191
        fseek($this->FileResource, $this->StripOffsets + ceil($row / $this->RowsPerStrip) * static::LEN_OFFSET);
192
193
        $firstColumnData = unpack('Voffset', fread($this->FileResource, static::LEN_OFFSET));
194
195
        fseek($this->FileResource, $firstColumnData['offset'] + ($this->NumDataCols * ($row % $this->RowsPerStrip) + $col) * $this->BytesPerSample);
196
197
        $elevation = unpack('velevation', fread($this->FileResource, $this->BytesPerSample))['elevation'];
198
199
        return ($elevation <= self::UNKNOWN || $elevation >= -self::UNKNOWN) ? false : $elevation;
200
    }
201
}
202