TWKBReader   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 40
eloc 129
c 1
b 0
f 0
dl 0
loc 241
rs 9.2

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getPolygon() 0 14 3
B getMulti() 0 31 8
A getLineString() 0 14 3
A read() 0 15 3
A getPoint() 0 25 4
F getGeometry() 0 88 19

How to fix   Complexity   

Complex Class

Complex classes like TWKBReader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TWKBReader, and based on these observations, apply Extract Interface, too.

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
 * @property Point $lastPoint
42
 */
43
trait TWKBReader
44
{
45
46
    /**
47
     * @var BinaryReader
48
     */
49
    protected $reader;
50
51
    /**
52
     * Read TWKB into geometry objects
53
     *
54
     * @param string $twkb Tiny Well-known-binary string
55
     * @param bool $isHexString If this is a hexadecimal string that is in need of packing
56
     * @return Geometry
57
     * @throws \Exception
58
     */
59
    public function read(string $twkb, bool $isHexString = false): Geometry
60
    {
61
        if ($isHexString) {
62
            $twkb = pack('H*', $twkb);
63
        }
64
65
        if (empty($twkb)) {
66
            throw new \Exception('Cannot read empty TWKB. Found ' . gettype($twkb));
67
        }
68
69
        $this->reader = new BinaryReader($twkb);
70
        $geometry = $this->getGeometry();
71
        $this->reader->close();
72
73
        return $geometry;
74
    }
75
76
    /**
77
     * @return Geometry
78
     * @throws \Exception
79
     */
80
    protected function getGeometry(): Geometry
81
    {
82
        $options = [];
83
        $type = $this->reader->readUInt8();
84
        $metadataHeader = $this->reader->readUInt8();
85
86
        $geometryType = $type & 0x0F;
87
        $options['precision'] = BinaryReader::zigZagDecode($type >> 4);
88
        $options['precisionFactor'] = pow(10, $options['precision']);
89
90
        $options['hasBoundingBox'] = ($metadataHeader >> 0 & 1) == 1;
91
        $options['hasSizeAttribute'] = ($metadataHeader >> 1 & 1) == 1;
92
        $options['hasIdList'] = ($metadataHeader >> 2 & 1) == 1;
93
        $options['hasExtendedPrecision'] = ($metadataHeader >> 3 & 1) == 1;
94
        $options['isEmpty'] = ($metadataHeader >> 4 & 1) == 1;
95
        $unused1 = ($metadataHeader >> 5 & 1) == 1;
96
        $unused2 = ($metadataHeader >> 6 & 1) == 1;
97
        $unused3 = ($metadataHeader >> 7 & 1) == 1;
98
99
        if ($options['hasExtendedPrecision']) {
100
            $extendedPrecision = $this->reader->readUInt8();
101
102
            $options['hasZ'] = ($extendedPrecision & 0x01) === 0x01;
103
            $options['hasM'] = ($extendedPrecision & 0x02) === 0x02;
104
105
            $options['zPrecision'] = ($extendedPrecision & 0x1C) >> 2;
106
            $options['zPrecisionFactor'] = pow(10, $options['zPrecision']);
107
108
            $options['mPrecision'] = ($extendedPrecision & 0xE0) >> 5;
109
            $options['mPrecisionFactor'] = pow(10, $options['mPrecision']);
110
        } else {
111
            $options['hasZ'] = false;
112
            $options['hasM'] = false;
113
            $options['zPrecisionFactor'] = 0;
114
            $options['mPrecisionFactor'] = 0;
115
        }
116
        if ($options['hasSizeAttribute']) {
117
            $options['remainderSize'] = $this->reader->readUVarInt();
118
        }
119
        if ($options['hasBoundingBox']) {
120
            $dimension = 2 + ($options['hasZ'] ? 1 : 0) + ($options['hasM'] ? 1 : 0);
121
            $precisions = [
122
                $options['precisionFactor'],
123
                $options['precisionFactor'],
124
                $options['hasZ'] ? $options['zPrecisionFactor'] : 0,
125
                $options['hasM'] ? $options['mPrecisionFactor'] : 0
126
            ];
127
128
            $bBoxMin = $bBoxMax = [];
129
            for ($i = 0; $i < $dimension; $i++) {
130
                $bBoxMin[$i] = $this->reader->readUVarInt() / $precisions[$i];
131
                $bBoxMax[$i] = $this->reader->readUVarInt() / $precisions[$i] + $bBoxMin[$i];
132
            }
133
            /** @noinspection PhpUndefinedVariableInspection (minimum 2 dimension) */
134
            $options['boundingBox'] = ['minXYZM' => $bBoxMin, 'maxXYZM' => $bBoxMax];
135
        }
136
137
        if ($unused1) {
138
            $this->reader->readUVarInt();
139
        }
140
        if ($unused2) {
141
            $this->reader->readUVarInt();
142
        }
143
        if ($unused3) {
144
            $this->reader->readUVarInt();
145
        }
146
147
        $this->lastPoint = new Point(0, 0, 0, 0);
148
149
        switch ($geometryType) {
150
            case 1:
151
                return $this->getPoint($options);
152
            case 2:
153
                return $this->getLineString($options);
154
            case 3:
155
                return $this->getPolygon($options);
156
            case 4:
157
                return $this->getMulti('Point', $options);
158
            case 5:
159
                return $this->getMulti('LineString', $options);
160
            case 6:
161
                return $this->getMulti('Polygon', $options);
162
            case 7:
163
                return $this->getMulti('Geometry', $options);
164
            default:
165
                throw new \Exception(
166
                    'Geometry type ' . $geometryType .
167
                        ' (' . (self::$typeMap[$geometryType] ?? 'unknown') . ') not supported'
168
                );
169
        }
170
    }
171
172
    /**
173
     * @param array<string, mixed> $options
174
     * @return Point
175
     * @throws \Exception
176
     */
177
    protected function getPoint(array $options): Point
178
    {
179
        if ($options['isEmpty']) {
180
            return new Point();
181
        }
182
        $x = round(
183
            $this->lastPoint->getX() + $this->reader->readSVarInt() / $options['precisionFactor'],
184
            $options['precision']
185
        );
186
        $y = round(
187
            $this->lastPoint->getY() + $this->reader->readSVarInt() / $options['precisionFactor'],
188
            $options['precision']
189
        );
190
        $z = $options['hasZ'] ? round(
191
            $this->lastPoint->getZ() + $this->reader->readSVarInt() / $options['zPrecisionFactor'],
192
            $options['zPrecision']
193
        ) : null;
194
        $m = $options['hasM'] ? round(
195
            $this->lastPoint->m() + $this->reader->readSVarInt() / $options['mPrecisionFactor'],
196
            $options['mprecision']
197
        ) : null;
198
199
        $this->lastPoint = new Point($x, $y, $z, $m);
200
        
201
        return $this->lastPoint;
202
    }
203
204
    /**
205
     * @param array<string, mixed> $options
206
     * @return LineString
207
     * @throws \Exception
208
     */
209
    protected function getLineString(array $options)
210
    {
211
        if ($options['isEmpty']) {
212
            return new LineString();
213
        }
214
215
        $numPoints = $this->reader->readUVarInt();
216
217
        $points = [];
218
        for ($i = 0; $i < $numPoints; $i++) {
219
            $points[] = $this->getPoint($options);
220
        }
221
222
        return new LineString($points);
223
    }
224
225
    /**
226
     * @param array<string, mixed> $options
227
     *
228
     * @return Polygon
229
     * @throws \Exception
230
     */
231
    protected function getPolygon(array $options)
232
    {
233
        if ($options['isEmpty']) {
234
            return new Polygon();
235
        }
236
237
        $ringCount = $this->reader->readUVarInt();
238
239
        $rings = [];
240
        for ($i = 0; $i < $ringCount; $i++) {
241
            $rings[] = $this->getLineString($options);
242
        }
243
244
        return new Polygon($rings, true);
245
    }
246
247
    /**
248
     * @param string $type
249
     * @param array<string, mixed> $options
250
     * @return MultiGeometry
251
     * @throws \Exception
252
     */
253
    private function getMulti(string $type, array $options): Geometry
254
    {
255
        $multiLength = $this->reader->readUVarInt();
256
257
        if ($options['hasIdList']) {
258
            $idList = [];
259
            for ($i = 0; $i < $multiLength; ++$i) {
260
                $idList[] = $this->reader->readSVarInt();
261
            }
262
        }
263
        
264
        $components = [];
265
        for ($i = 0; $i < $multiLength; $i++) {
266
            if ($type !== 'Geometry') {
267
                $func = 'get' . $type;
268
                $components[] = $this->$func($options);
269
            } else {
270
                $components[] = $this->getGeometry();
271
            }
272
        }
273
274
        switch ($type) {
275
            case 'Point':
276
                return new MultiPoint($components);
277
            case 'LineString':
278
                return new MultiLineString($components);
279
            case 'Polygon':
280
                return new MultiPolygon($components);
281
        }
282
        
283
        return new GeometryCollection($components);
284
    }
285
}
286