Passed
Push — master ( 41a4af...bb1b1f )
by Swen
03:21
created

TWKBReader::getPolygon()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 14
rs 10
cc 3
nc 3
nop 1
1
<?php
2
/**
3
 * This file contains the BinaryReader class.
4
 * For more information see the class description below.
5
 *
6
 * @author Peter Bathory <[email protected]>
7
 * @since 2016-02-18
8
 *
9
 * This code is open-source and licenced under the Modified BSD License.
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
namespace geoPHP\Adapter;
14
15
use geoPHP\Geometry\Geometry;
16
use geoPHP\Geometry\GeometryCollection;
17
use geoPHP\Geometry\Point;
18
use geoPHP\Geometry\MultiPoint;
19
use geoPHP\Geometry\LineString;
20
use geoPHP\Geometry\MultiLineString;
21
use geoPHP\Geometry\Polygon;
22
use geoPHP\Geometry\MultiPolygon;
23
use geoPHP\Geometry\MultiGeometry;
24
25
/**
26
 * PHP Geometry <-> TWKB encoder/decoder
27
 *
28
 * "Tiny Well-known Binary is is a multi-purpose format for serializing vector geometry data into a byte buffer,
29
 * with an emphasis on minimizing size of the buffer."
30
 * @see https://github.com/TWKB/Specification/blob/master/twkb.md
31
 *
32
 * This implementation supports:
33
 * - reading and writing all geometry types (1-7)
34
 * - empty geometries
35
 * - extended precision (Z, M coordinates; custom precision)
36
 * Partially supports:
37
 * - bounding box: can read and write, but don't store readed boxes (API missing)
38
 * - size attribute: can read and write size attribute, but seeking is not supported
39
 * - ID list: can read and write, but API is completely missing
40
 */
41
trait TWKBReader
42
{
43
44
    /**
45
     * @var BinaryReader
46
     */
47
    protected $reader;
48
49
    /**
50
     * Read TWKB into geometry objects
51
     *
52
     * @param string $twkb Tiny Well-known-binary string
53
     * @param bool $isHexString If this is a hexadecimal string that is in need of packing
54
     * @return Geometry
55
     * @throws \Exception
56
     */
57
    public function read(string $twkb, bool $isHexString = false): Geometry
58
    {
59
        if ($isHexString) {
60
            $twkb = pack('H*', $twkb);
61
        }
62
63
        if (empty($twkb)) {
64
            throw new \Exception('Cannot read empty TWKB. Found ' . gettype($twkb));
65
        }
66
67
        $this->reader = new BinaryReader($twkb);
68
        $geometry = $this->getGeometry();
69
        $this->reader->close();
70
71
        return $geometry;
72
    }
73
74
    /**
75
     * @return Geometry
76
     * @throws \Exception
77
     */
78
    protected function getGeometry(): Geometry
79
    {
80
        $options = [];
81
        $type = $this->reader->readUInt8();
82
        $metadataHeader = $this->reader->readUInt8();
83
84
        $geometryType = $type & 0x0F;
85
        $options['precision'] = BinaryReader::zigZagDecode($type >> 4);
86
        $options['precisionFactor'] = pow(10, $options['precision']);
87
88
        $options['hasBoundingBox'] = ($metadataHeader >> 0 & 1) == 1;
89
        $options['hasSizeAttribute'] = ($metadataHeader >> 1 & 1) == 1;
90
        $options['hasIdList'] = ($metadataHeader >> 2 & 1) == 1;
91
        $options['hasExtendedPrecision'] = ($metadataHeader >> 3 & 1) == 1;
92
        $options['isEmpty'] = ($metadataHeader >> 4 & 1) == 1;
93
        $unused1 = ($metadataHeader >> 5 & 1) == 1;
94
        $unused2 = ($metadataHeader >> 6 & 1) == 1;
95
        $unused3 = ($metadataHeader >> 7 & 1) == 1;
96
97
        if ($options['hasExtendedPrecision']) {
98
            $extendedPrecision = $this->reader->readUInt8();
99
100
            $options['hasZ'] = ($extendedPrecision & 0x01) === 0x01;
101
            $options['hasM'] = ($extendedPrecision & 0x02) === 0x02;
102
103
            $options['zPrecision'] = ($extendedPrecision & 0x1C) >> 2;
104
            $options['zPrecisionFactor'] = pow(10, $options['zPrecision']);
105
106
            $options['mPrecision'] = ($extendedPrecision & 0xE0) >> 5;
107
            $options['mPrecisionFactor'] = pow(10, $options['mPrecision']);
108
        } else {
109
            $options['hasZ'] = false;
110
            $options['hasM'] = false;
111
            $options['zPrecisionFactor'] = 0;
112
            $options['mPrecisionFactor'] = 0;
113
        }
114
        if ($options['hasSizeAttribute']) {
115
            $options['remainderSize'] = $this->reader->readUVarInt();
116
        }
117
        if ($options['hasBoundingBox']) {
118
            $dimension = 2 + ($options['hasZ'] ? 1 : 0) + ($options['hasM'] ? 1 : 0);
119
            $precisions = [
120
                $options['precisionFactor'],
121
                $options['precisionFactor'],
122
                $options['hasZ'] ? $options['zPrecisionFactor'] : 0,
123
                $options['hasM'] ? $options['mPrecisionFactor'] : 0
124
            ];
125
126
            $bBoxMin = $bBoxMax = [];
127
            for ($i = 0; $i < $dimension; $i++) {
128
                $bBoxMin[$i] = $this->reader->readUVarInt() / $precisions[$i];
129
                $bBoxMax[$i] = $this->reader->readUVarInt() / $precisions[$i] + $bBoxMin[$i];
130
            }
131
            /** @noinspection PhpUndefinedVariableInspection (minimum 2 dimension) */
132
            $options['boundingBox'] = ['minXYZM' => $bBoxMin, 'maxXYZM' => $bBoxMax];
133
        }
134
135
        if ($unused1) {
136
            $this->reader->readUVarInt();
137
        }
138
        if ($unused2) {
139
            $this->reader->readUVarInt();
140
        }
141
        if ($unused3) {
142
            $this->reader->readUVarInt();
143
        }
144
145
        $this->lastPoint = new Point(0, 0, 0, 0);
0 ignored issues
show
Bug Best Practice introduced by
The property lastPoint does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
146
147
        switch ($geometryType) {
148
            case 1:
149
                return $this->getPoint($options);
150
            case 2:
151
                return $this->getLineString($options);
152
            case 3:
153
                return $this->getPolygon($options);
154
            case 4:
155
                return $this->getMulti('Point', $options);
156
            case 5:
157
                return $this->getMulti('LineString', $options);
158
            case 6:
159
                return $this->getMulti('Polygon', $options);
160
            case 7:
161
                return $this->getMulti('Geometry', $options);
162
            default:
163
                throw new \Exception(
164
                    'Geometry type ' . $geometryType .
165
                        ' (' . (self::$typeMap[$geometryType] ?? 'unknown') . ') not supported'
166
                );
167
        }
168
    }
169
170
    /**
171
     * @param array<string, mixed> $options
172
     * @return Point
173
     * @throws \Exception
174
     */
175
    protected function getPoint(array $options): Point
176
    {
177
        if ($options['isEmpty']) {
178
            return new Point();
179
        }
180
        $x = round(
181
            $this->lastPoint->getX() + $this->reader->readSVarInt() / $options['precisionFactor'],
182
            $options['precision']
183
        );
184
        $y = round(
185
            $this->lastPoint->getY() + $this->reader->readSVarInt() / $options['precisionFactor'],
186
            $options['precision']
187
        );
188
        $z = $options['hasZ'] ? round(
189
            $this->lastPoint->getZ() + $this->reader->readSVarInt() / $options['zPrecisionFactor'],
190
            $options['zPrecision']
191
        ) : null;
192
        $m = $options['hasM'] ? round(
193
            $this->lastPoint->m() + $this->reader->readSVarInt() / $options['mPrecisionFactor'],
194
            $options['mprecision']
195
        ) : null;
196
197
        $this->lastPoint = new Point($x, $y, $z, $m);
0 ignored issues
show
Bug Best Practice introduced by
The property lastPoint does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
198
        
199
        return $this->lastPoint;
200
    }
201
202
    /**
203
     * @param array<string, mixed> $options
204
     * @return LineString
205
     * @throws \Exception
206
     */
207
    protected function getLineString(array $options)
208
    {
209
        if ($options['isEmpty']) {
210
            return new LineString();
211
        }
212
213
        $numPoints = $this->reader->readUVarInt();
214
215
        $points = [];
216
        for ($i = 0; $i < $numPoints; $i++) {
217
            $points[] = $this->getPoint($options);
218
        }
219
220
        return new LineString($points);
221
    }
222
223
    /**
224
     * @param array<string, mixed> $options
225
     *
226
     * @return Polygon
227
     * @throws \Exception
228
     */
229
    protected function getPolygon(array $options)
230
    {
231
        if ($options['isEmpty']) {
232
            return new Polygon();
233
        }
234
235
        $ringCount = $this->reader->readUVarInt();
236
237
        $rings = [];
238
        for ($i = 0; $i < $ringCount; $i++) {
239
            $rings[] = $this->getLineString($options);
240
        }
241
242
        return new Polygon($rings, true);
243
    }
244
245
    /**
246
     * @param string $type
247
     * @param array<string, mixed> $options
248
     * @return MultiGeometry
249
     * @throws \Exception
250
     */
251
    private function getMulti(string $type, array $options): Geometry
252
    {
253
        $multiLength = $this->reader->readUVarInt();
254
255
        if ($options['hasIdList']) {
256
            $idList = [];
257
            for ($i = 0; $i < $multiLength; ++$i) {
258
                $idList[] = $this->reader->readSVarInt();
259
            }
260
        }
261
262
        $components = [];
263
        for ($i = 0; $i < $multiLength; $i++) {
264
            if ($type !== 'Geometry') {
265
                $func = 'get' . $type;
266
                $components[] = $this->$func($options);
267
            } else {
268
                $components[] = $this->getGeometry();
269
            }
270
        }
271
        
272
        switch ($type) {
273
            case 'Point':
274
                return new MultiPoint($components);
275
            case 'LineString':
276
                return new MultiLineString($components);
277
            case 'Polygon':
278
                return new MultiPolygon($components);
279
        }
280
        
281
        return new GeometryCollection($components);
282
    }
283
}
284