Passed
Pull Request — master (#4)
by Mark
01:50
created

WKB::writeLineString()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace geoPHP\Adapter;
4
5
use geoPHP\Geometry\Geometry;
6
use geoPHP\Geometry\GeometryCollection;
7
use geoPHP\Geometry\Point;
0 ignored issues
show
Bug introduced by
The type geoPHP\Geometry\Point was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use geoPHP\Geometry\MultiPoint;
9
use geoPHP\Geometry\LineString;
10
use geoPHP\Geometry\MultiLineString;
11
use geoPHP\Geometry\Polygon;
12
use geoPHP\Geometry\MultiPolygon;
13
14
/*
15
 * (c) Patrick Hayes
16
 *
17
 * This code is open-source and licenced under the Modified BSD License.
18
 * For the full copyright and license information, please view the LICENSE
19
 * file that was distributed with this source code.
20
 */
21
22
/**
23
 * PHP Geometry/WKB encoder/decoder
24
 * Reader can decode EWKB too. Writer always encodes valid WKBs
25
 *
26
 */
27
class WKB implements GeoAdapter
28
{
29
    const Z_MASK = 0x80000000;
30
    const M_MASK = 0x40000000;
31
    const SRID_MASK = 0x20000000;
32
    const WKB_XDR = 1;
33
    const WKB_NDR = 0;
34
35
    protected $hasZ = false;
36
37
    protected $hasM = false;
38
39
    protected $hasSRID = false;
40
41
    protected $SRID = null;
42
43
    protected $dimension = 2;
44
45
    /** @var  BinaryReader $reader */
46
    protected $reader;
47
48
    /** @var  BinaryWriter $writer */
49
    protected $writer;
50
51
    /** @var array Maps Geometry types to WKB type codes */
52
    public static $typeMap = [
53
            Geometry::POINT               => 1,
54
            Geometry::LINE_STRING         => 2,
55
            Geometry::POLYGON             => 3,
56
            Geometry::MULTI_POINT         => 4,
57
            Geometry::MULTI_LINE_STRING   => 5,
58
            Geometry::MULTI_POLYGON       => 6,
59
            Geometry::GEOMETRY_COLLECTION => 7,
60
            //Not supported types:
61
            Geometry::CIRCULAR_STRING     => 8,
62
            Geometry::COMPOUND_CURVE      => 9,
63
            Geometry::CURVE_POLYGON       => 10,
64
            Geometry::MULTI_CURVE         => 11,
65
            Geometry::MULTI_SURFACE       => 12,
66
            Geometry::CURVE               => 13,
67
            Geometry::SURFACE             => 14,
68
            Geometry::POLYHEDRAL_SURFACE  => 15,
69
            Geometry::TIN                 => 16,
70
            Geometry::TRIANGLE            => 17,
71
    ];
72
73
    /**
74
     * Read WKB into geometry objects
75
     *
76
     * @param string $wkb         Well-known-binary string
77
     * @param bool   $isHexString If this is a hexadecimal string that is in need of packing
78
     *
79
     * @return Geometry
80
     *
81
     * @throws \Exception
82
     */
83
    public function read($wkb, $isHexString = false)
84
    {
85
        if ($isHexString) {
86
            $wkb = pack('H*', $wkb);
87
        }
88
89
        if (empty($wkb)) {
90
            throw new \Exception('Cannot read empty WKB geometry. Found ' . gettype($wkb));
91
        }
92
93
        $this->reader = new BinaryReader($wkb);
94
95
        $geometry = $this->getGeometry();
96
97
        $this->reader->close();
98
99
        return $geometry;
100
    }
101
102
    /**
103
     * @return Geometry
104
     * @throws \Exception
105
     */
106
    protected function getGeometry()
107
    {
108
        $this->hasZ = false;
109
        $this->hasM = false;
110
        $SRID = null;
111
112
        $this->reader->setEndianness(
113
            $this->reader->readSInt8() === self::WKB_XDR ? BinaryReader::LITTLE_ENDIAN : BinaryReader::BIG_ENDIAN
114
        );
115
116
        $wkbType = $this->reader->readUInt32();
117
118
        if (($wkbType & $this::SRID_MASK) === $this::SRID_MASK) {
119
            $SRID = $this->reader->readUInt32();
120
        }
121
        $geometryType = null;
122
        if ($wkbType >= 1000 && $wkbType < 2000) {
123
            $this->hasZ = true;
124
            $geometryType = $wkbType - 1000;
125
        } elseif ($wkbType >= 2000 && $wkbType < 3000) {
126
            $this->hasM = true;
127
            $geometryType = $wkbType - 2000;
128
        } elseif ($wkbType >= 3000 && $wkbType < 4000) {
129
            $this->hasZ = true;
130
            $this->hasM = true;
131
            $geometryType = $wkbType - 3000;
132
        }
133
134
        if ($wkbType & $this::Z_MASK) {
135
            $this->hasZ = true;
136
        }
137
        if ($wkbType & $this::M_MASK) {
138
            $this->hasM = true;
139
        }
140
        $this->dimension = 2 + ($this->hasZ ? 1 : 0) + ($this->hasM ? 1 : 0);
141
142
        if (!$geometryType) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $geometryType of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
143
            $geometryType = $wkbType & 0xF; // remove any masks from type
144
        }
145
        $geometry = null;
146
        switch ($geometryType) {
147
            case 1:
148
                $geometry = $this->getPoint();
149
                break;
150
            case 2:
151
                $geometry = $this->getLineString();
152
                break;
153
            case 3:
154
                $geometry = $this->getPolygon();
155
                break;
156
            case 4:
157
                $geometry = $this->getMulti('Point');
158
                break;
159
            case 5:
160
                $geometry = $this->getMulti('LineString');
161
                break;
162
            case 6:
163
                $geometry = $this->getMulti('Polygon');
164
                break;
165
            case 7:
166
                $geometry = $this->getMulti('Geometry');
167
                break;
168
            default:
169
                throw new \Exception(
170
                    'Geometry type ' . $geometryType .
171
                    ' (' . (array_search($geometryType, self::$typeMap) ?: 'unknown') . ') not supported'
172
                );
173
        }
174
        if ($geometry && $SRID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $SRID of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
175
            $geometry->setSRID($SRID);
176
        }
177
        return $geometry;
178
    }
179
180
    protected function getPoint()
181
    {
182
        $coordinates = $this->reader->readDoubles($this->dimension * 8);
183
        $point = null;
184
        switch (count($coordinates)) {
185
            case 2:
186
                $point = new Point($coordinates[0], $coordinates[1]);
187
                break;
188
            case 3:
189
                if ($this->hasZ) {
190
                    $point = new Point($coordinates[0], $coordinates[1], $coordinates[2]);
191
                } else {
192
                    $point = new Point($coordinates[0], $coordinates[1], null, $coordinates[2]);
193
                }
194
                break;
195
            case 4:
196
                $point = new Point($coordinates[0], $coordinates[1], $coordinates[2], $coordinates[3]);
197
                break;
198
        }
199
        return $point;
200
    }
201
202
    protected function getLineString()
203
    {
204
        // Get the number of points expected in this string out of the first 4 bytes
205
        $lineLength = $this->reader->readUInt32();
206
207
        // Return an empty linestring if there is no line-length
208
        if (!$lineLength) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lineLength of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
209
            return new LineString();
210
        }
211
212
        $components = [];
213
        for ($i = 0; $i < $lineLength; ++$i) {
214
            $point = $this->getPoint();
215
            if ($point) {
216
                $components[] = $point;
217
            }
218
        }
219
        return new LineString($components);
220
    }
221
222
    protected function getPolygon()
223
    {
224
        // Get the number of linestring expected in this poly out of the first 4 bytes
225
        $polyLength = $this->reader->readUInt32();
226
227
        $components = [];
228
        $i = 1;
229
        while ($i <= $polyLength) {
230
            $ring = $this->getLineString();
231
            if (!$ring->isEmpty()) {
232
                $components[] = $ring;
233
            }
234
            $i++;
235
        }
236
237
        return new Polygon($components);
238
    }
239
240
    protected function getMulti($type)
241
    {
242
        // Get the number of items expected in this multi out of the first 4 bytes
243
        $multiLength = $this->reader->readUInt32();
244
245
        $components = [];
246
        for ($i = 0; $i < $multiLength; $i++) {
247
            $component = $this->getGeometry();
248
            $component->setSRID(null);
249
            $components[] = $component;
250
        }
251
        switch ($type) {
252
            case 'Point':
253
                return new MultiPoint($components);
254
            case 'LineString':
255
                return new MultiLineString($components);
256
            case 'Polygon':
257
                return new MultiPolygon($components);
258
            case 'Geometry':
259
                return new GeometryCollection($components);
260
        }
261
        return null;
262
    }
263
264
    /**
265
     * Serialize geometries into WKB string.
266
     *
267
     * @param Geometry $geometry The geometry
268
     * @param boolean $writeAsHex Write the result in binary or hexadecimal system
269
     * @param boolean $bigEndian Write in BigEndian or LittleEndian byte order
270
     *
271
     * @return string The WKB string representation of the input geometries
272
     */
273
    public function write(Geometry $geometry, $writeAsHex = false, $bigEndian = false)
274
    {
275
276
        $this->writer = new BinaryWriter($bigEndian ? BinaryWriter::BIG_ENDIAN : BinaryWriter::LITTLE_ENDIAN);
277
278
        $wkb = $this->writeGeometry($geometry);
279
280
        return $writeAsHex ? current(unpack('H*', $wkb)) : $wkb;
281
    }
282
283
    /**
284
     * @param Geometry $geometry
285
     * @return string
286
     */
287
    protected function writeGeometry($geometry)
288
    {
289
        $this->hasZ = $geometry->hasZ();
290
        $this->hasM = $geometry->isMeasured();
291
292
        $wkb = $this->writer->writeSInt8($this->writer->isBigEndian() ? self::WKB_NDR : self::WKB_XDR);
293
        $wkb .= $this->writeType($geometry);
294
        switch ($geometry->geometryType()) {
295
            case Geometry::POINT:
296
                /** @var Point $geometry */
297
                $wkb .= $this->writePoint($geometry);
298
                break;
299
            case Geometry::LINE_STRING:
300
                /** @var LineString $geometry */
301
                $wkb .= $this->writeLineString($geometry);
302
                break;
303
            case Geometry::POLYGON:
304
                /** @var Polygon $geometry */
305
                $wkb .= $this->writePolygon($geometry);
306
                break;
307
            case Geometry::MULTI_POINT:
308
                /** @var MultiPoint $geometry */
309
                $wkb .= $this->writeMulti($geometry);
310
                break;
311
            case Geometry::MULTI_LINE_STRING:
312
                /** @var MultiLineString $geometry */
313
                $wkb .= $this->writeMulti($geometry);
314
                break;
315
            case Geometry::MULTI_POLYGON:
316
                /** @var MultiPolygon $geometry */
317
                $wkb .= $this->writeMulti($geometry);
318
                break;
319
            case Geometry::GEOMETRY_COLLECTION:
320
                /** @var GeometryCollection $geometry */
321
                $wkb .= $this->writeMulti($geometry);
322
                break;
323
        }
324
        return $wkb;
325
    }
326
327
    /**
328
     * @param Point $point
329
     * @return string
330
     */
331
    protected function writePoint($point)
332
    {
333
        if ($point->isEmpty()) {
334
            return $this->writer->writeDouble(NAN) . $this->writer->writeDouble(NAN);
335
        }
336
        $wkb = $this->writer->writeDouble($point->x()) . $this->writer->writeDouble($point->y());
337
338
        if ($this->hasZ) {
339
            $wkb .= $this->writer->writeDouble($point->z());
340
        }
341
        if ($this->hasM) {
342
            $wkb .= $this->writer->writeDouble($point->m());
343
        }
344
        return $wkb;
345
    }
346
347
    /**
348
     * @param LineString $line
349
     * @return string
350
     */
351
    protected function writeLineString($line)
352
    {
353
        // Set the number of points in this line
354
        $wkb = $this->writer->writeUInt32($line->numPoints());
355
356
        // Set the coords
357
        foreach ($line->getComponents() as $i => $point) {
358
            $wkb .= $this->writePoint($point);
359
        }
360
361
        return $wkb;
362
    }
363
364
    /**
365
     * @param Polygon $poly
366
     * @return string
367
     */
368
    protected function writePolygon($poly)
369
    {
370
        // Set the number of lines in this poly
371
        $wkb = $this->writer->writeUInt32($poly->numGeometries());
372
373
        // Write the lines
374
        foreach ($poly->getComponents() as $line) {
375
            $wkb .= $this->writeLineString($line);
376
        }
377
378
        return $wkb;
379
    }
380
381
    /**
382
     * @param MultiPoint|MultiPolygon|MultiLineString|GeometryCollection $geometry
383
     * @return string
384
     */
385
    protected function writeMulti($geometry)
386
    {
387
        // Set the number of components
388
        $wkb = $this->writer->writeUInt32($geometry->numGeometries());
389
390
        // Write the components
391
        foreach ($geometry->getComponents() as $component) {
392
            $wkb .= $this->writeGeometry($component);
393
        }
394
395
        return $wkb;
396
    }
397
398
    /**
399
     * @param Geometry $geometry
400
     * @param bool $writeSRID
401
     * @return string
402
     */
403
    protected function writeType($geometry, $writeSRID = false)
404
    {
405
        $type = self::$typeMap[$geometry->geometryType()];
406
        // Binary OR to mix in additional properties
407
        if ($this->hasZ) {
408
            $type = $type | $this::Z_MASK;
409
        }
410
        if ($this->hasM) {
411
            $type = $type | $this::M_MASK;
412
        }
413
        if ($geometry->SRID() && $writeSRID) {
414
            $type = $type | $this::SRID_MASK;
415
        }
416
        return $this->writer->writeUInt32($type) .
417
            ($geometry->SRID() && $writeSRID ? $this->writer->writeUInt32($this->SRID) : '');
418
    }
419
}
420