Issues (44)

src/Adapter/WKB.php (2 issues)

1
<?php
2
namespace geoPHP\Adapter;
3
4
use geoPHP\Geometry\Geometry;
5
use geoPHP\Geometry\GeometryCollection;
6
use geoPHP\Geometry\Point;
7
use geoPHP\Geometry\MultiPoint;
8
use geoPHP\Geometry\LineString;
9
use geoPHP\Geometry\MultiLineString;
10
use geoPHP\Geometry\Polygon;
11
use geoPHP\Geometry\MultiPolygon;
12
use geoPHP\Exception\InvalidGeometryException;
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
30
    const Z_MASK = 0x80000000;
31
    const M_MASK = 0x40000000;
32
    const SRID_MASK = 0x20000000;
33
    const WKB_XDR = 1;
34
    const WKB_NDR = 0;
35
36
    /**
37
     * @var bool $hasZ
38
     */
39
    protected $hasZ = false;
40
    
41
    /**
42
     * @var bool $hasM
43
     */
44
    protected $hasM = false;
45
    
46
    /**
47
     * @var bool $hasSRID
48
     */
49
    protected $hasSRID = false;
50
    
51
    /**
52
     * @var int $SRID
53
     */
54
    protected $SRID;
55
    
56
    /**
57
     * @var int $dimension
58
     */
59
    protected $dimension = 2;
60
61
    /**
62
     * @var BinaryReader $reader
63
     */
64
    protected $reader;
65
66
    /**
67
     * @var BinaryWriter $writer
68
     */
69
    protected $writer;
70
71
    /**
72
     * @var array<int> Maps Geometry types to WKB type codes
73
     */
74
    public static $typeMap = [
75
        Geometry::POINT => 1,
76
        Geometry::LINESTRING => 2,
77
        Geometry::POLYGON => 3,
78
        Geometry::MULTI_POINT => 4,
79
        Geometry::MULTI_LINESTRING => 5,
80
        Geometry::MULTI_POLYGON => 6,
81
        Geometry::GEOMETRY_COLLECTION => 7,
82
        //Not supported types:
83
        Geometry::CIRCULAR_STRING => 8,
84
        Geometry::COMPOUND_CURVE => 9,
85
        Geometry::CURVE_POLYGON => 10,
86
        Geometry::MULTI_CURVE => 11,
87
        Geometry::MULTI_SURFACE => 12,
88
        Geometry::CURVE => 13,
89
        Geometry::SURFACE => 14,
90
        Geometry::POLYHEDRAL_SURFACE => 15,
91
        Geometry::TIN => 16,
92
        Geometry::TRIANGLE => 17
93
    ];
94
95
    /**
96
     * Read WKB into geometry objects
97
     *
98
     * @param string $wkb Well-known-binary string
99
     * @param bool $isHexString If this is a hexadecimal string that is in need of packing
100
     * @return Geometry
101
     * @throws \Exception
102
     */
103
    public function read(string $wkb, bool $isHexString = false): Geometry
104
    {
105
        if ($isHexString) {
106
            $wkb = pack('H*', $wkb);
107
        }
108
109
        if (empty($wkb)) {
110
            throw new \Exception('Cannot read empty WKB geometry. Found ' . gettype($wkb));
111
        }
112
113
        $this->reader = new BinaryReader($wkb);
114
        $geometry = $this->getGeometry();
115
        $this->reader->close();
116
117
        return $geometry;
118
    }
119
120
    /**
121
     * @return Geometry
122
     * @throws \Exception
123
     */
124
    protected function getGeometry(): Geometry
125
    {
126
        $this->hasZ = false;
127
        $this->hasM = false;
128
        $SRID = null;
129
130
        $this->reader->setEndianness(
131
            $this->reader->readSInt8() === self::WKB_XDR ? BinaryReader::LITTLE_ENDIAN : BinaryReader::BIG_ENDIAN
132
        );
133
134
        $wkbType = $this->reader->readUInt32();
135
136
        if (($wkbType & $this::SRID_MASK) === $this::SRID_MASK) {
137
            $SRID = $this->reader->readUInt32();
138
        }
139
        $geometryType = null;
140
        if ($wkbType >= 1000 && $wkbType < 2000) {
141
            $this->hasZ = true;
142
            $geometryType = $wkbType - 1000;
143
        } elseif ($wkbType >= 2000 && $wkbType < 3000) {
144
            $this->hasM = true;
145
            $geometryType = $wkbType - 2000;
146
        } elseif ($wkbType >= 3000 && $wkbType < 4000) {
147
            $this->hasZ = true;
148
            $this->hasM = true;
149
            $geometryType = $wkbType - 3000;
150
        }
151
152
        if ($wkbType & $this::Z_MASK) {
153
            $this->hasZ = true;
154
        }
155
        if ($wkbType & $this::M_MASK) {
156
            $this->hasM = true;
157
        }
158
        $this->dimension = 2 + ($this->hasZ ? 1 : 0) + ($this->hasM ? 1 : 0);
159
160
        if ($geometryType === null) {
161
            $geometryType = $wkbType & 0xF; // remove any masks from type
162
        }
163
        $geometry = null;
164
        switch ($geometryType) {
165
            case 1:
166
                $geometry = $this->getPoint();
167
                break;
168
            case 2:
169
                $geometry = $this->getLineString();
170
                break;
171
            case 3:
172
                $geometry = $this->getPolygon();
173
                break;
174
            case 4:
175
                $geometry = $this->getMulti('Point');
176
                break;
177
            case 5:
178
                $geometry = $this->getMulti('LineString');
179
                break;
180
            case 6:
181
                $geometry = $this->getMulti('Polygon');
182
                break;
183
            case 7:
184
                $geometry = $this->getMulti('Geometry');
185
                break;
186
            default:
187
                throw new \Exception(
188
                    'Geometry type ' . $geometryType .
189
                    ' (' . (self::$typeMap[$geometryType] ?? 'unknown') . ') not supported'
190
                );
191
        }
192
        if ($SRID !== null) {
193
            $geometry->setSRID($SRID);
194
        }
195
        
196
        return $geometry;
197
    }
198
199
    /**
200
     * @return Point
201
     */
202
    protected function getPoint(): Point
203
    {
204
        $coordinates = $this->reader->readDoubles($this->dimension * 8);
205
        
206
        switch (count($coordinates)) {
207
            case 2:
208
                return new Point($coordinates[0], $coordinates[1]);
209
            case 3:
210
                return $this->hasZ ?
211
                    new Point($coordinates[0], $coordinates[1], $coordinates[2]) :
212
                    new Point($coordinates[0], $coordinates[1], null, $coordinates[2]);
213
            case 4:
214
                return new Point($coordinates[0], $coordinates[1], $coordinates[2], $coordinates[3]);
215
        }
216
        
217
        return new Point;
218
    }
219
220
    /**
221
     * @return LineString
222
     */
223
    protected function getLineString(): LineString
224
    {
225
        // Get the number of points expected in this string out of the first 4 bytes
226
        $lineLength = $this->reader->readUInt32();
227
228
        // Return an empty linestring if there is no line-length
229
        if ($lineLength === null) {
230
            return new LineString();
231
        }
232
233
        $components = [];
234
        for ($i = 0; $i < $lineLength; ++$i) {
235
            $components[] = $this->getPoint();
236
        }
237
        
238
        return new LineString($components);
239
    }
240
241
    /**
242
     * @return Polygon
243
     */
244
    protected function getPolygon(): Polygon
245
    {
246
        // Get the number of linestring expected in this poly out of the first 4 bytes
247
        $polyLength = $this->reader->readUInt32();
248
249
        $components = [];
250
        $i = 1;
251
        while ($i <= $polyLength) {
252
            $ring = $this->getLineString();
253
            if (!$ring->isEmpty()) {
254
                $components[] = $ring;
255
            }
256
            $i++;
257
        }
258
259
        return new Polygon($components);
260
    }
261
262
    /**
263
     * @param string $type
264
     * @return MultiPoint|MultiLineString|MultiPolygon|GeometryCollection
265
     */
266
    private function getMulti(string $type): Geometry
267
    {
268
        // Get the number of items expected in this multi out of the first 4 bytes
269
        $multiLength = $this->reader->readUInt32();
270
271
        $components = [];
272
        for ($i = 0; $i < $multiLength; ++$i) {
273
            $component = $this->getGeometry();
274
            $component->setSRID(null);
275
            $components[] = $component;
276
        }
277
        
278
        switch ($type) {
279
            case 'Point':
280
                /** @var Point[] $components */
281
                return new MultiPoint($components);
282
            case 'LineString':
283
                /** @var LineString[] $components */
284
                return new MultiLineString($components);
285
            case 'Polygon':
286
                /** @var Polygon[] $components */
287
                return new MultiPolygon($components);
288
        }
289
        
290
        return new GeometryCollection($components);
291
    }
292
293
    /**
294
     * Serialize geometries into WKB string.
295
     *
296
     * @param Geometry $geometry The geometry
297
     * @param bool $writeAsHex Write the result in binary or hexadecimal system. Default false.
298
     * @param bool $bigEndian Write in BigEndian byte order. Default false.
299
     *
300
     * @return string The WKB string representation of the input geometries
301
     */
302
    public function write(Geometry $geometry, bool $writeAsHex = false, bool $bigEndian = false): string
303
    {
304
        $this->writer = new BinaryWriter($bigEndian ? BinaryWriter::BIG_ENDIAN : BinaryWriter::LITTLE_ENDIAN);
305
        $wkb = $this->writeGeometry($geometry);
306
307
        $data = unpack('H*', $wkb);
308
        return $writeAsHex ? ($data ? current($data) : '') : $wkb;
309
    }
310
311
    /**
312
     * @param Geometry $geometry
313
     * @return string
314
     */
315
    protected function writeGeometry(Geometry $geometry): string
316
    {
317
        $this->hasZ = $geometry->hasZ();
318
        $this->hasM = $geometry->isMeasured();
319
320
        $wkb = $this->writer->writeSInt8($this->writer->isBigEndian() ? self::WKB_NDR : self::WKB_XDR);
321
        $wkb .= $this->writeType($geometry);
322
        
323
        switch ($geometry->geometryType()) {
324
            case Geometry::POINT:
325
                /** @var Point $geometry */
326
                $wkb .= $this->writePoint($geometry);
327
                break;
328
            case Geometry::LINESTRING:
329
                /** @var LineString $geometry */
330
                $wkb .= $this->writeLineString($geometry);
331
                break;
332
            case Geometry::POLYGON:
333
                /** @var Polygon $geometry */
334
                $wkb .= $this->writePolygon($geometry);
335
                break;
336
            case Geometry::MULTI_POINT:
337
                /** @var MultiPoint $geometry */
338
                $wkb .= $this->writeMulti($geometry);
339
                break;
340
            case Geometry::MULTI_LINESTRING:
341
                /** @var MultiLineString $geometry */
342
                $wkb .= $this->writeMulti($geometry);
343
                break;
344
            case Geometry::MULTI_POLYGON:
345
                /** @var MultiPolygon $geometry */
346
                $wkb .= $this->writeMulti($geometry);
347
                break;
348
            case Geometry::GEOMETRY_COLLECTION:
349
                /** @var GeometryCollection $geometry */
350
                $wkb .= $this->writeMulti($geometry);
351
                break;
352
        }
353
        
354
        return $wkb;
355
    }
356
357
    /**
358
     * @param Point $point
359
     * @return string
360
     * @throws InvalidGeometryException
361
     */
362
    protected function writePoint(Point $point): string
363
    {
364
        if ($point->isEmpty()) {
365
            #return $this->writer->writeDouble(null) . $this->writer->writeDouble(null);
366
            
367
            // GEOS throws an IllegalArgumentException with "Empty Points cannot be represented in WKB."
368
            throw new InvalidGeometryException("Empty Points cannot be represented in WKB");
369
        }
370
        $wkb = $this->writer->writeDouble($point->getX()) . $this->writer->writeDouble($point->getY());
371
372
        if ($this->hasZ) {
373
            $wkb .= $this->writer->writeDouble($point->getZ());
374
        }
375
        if ($this->hasM) {
376
            $wkb .= $this->writer->writeDouble($point->m());
377
        }
378
        
379
        return $wkb;
380
    }
381
382
    /**
383
     * @param LineString $line
384
     * @return string
385
     */
386
    protected function writeLineString(LineString $line): string
387
    {
388
        // Set the number of points in this line
389
        $wkb = $this->writer->writeUInt32($line->numPoints());
390
391
        // Set the coords
392
        foreach ($line->getComponents() as $point) {
393
            $wkb .= $this->writePoint($point);
394
        }
395
396
        return $wkb;
397
    }
398
399
    /**
400
     * @param Polygon $poly
401
     * @return string
402
     */
403
    protected function writePolygon(Polygon $poly): string
404
    {
405
        // Set the number of lines in this poly
406
        $wkb = $this->writer->writeUInt32($poly->numGeometries());
407
408
        // Write the lines
409
        foreach ($poly->getComponents() as $line) {
410
            $wkb .= $this->writeLineString($line);
411
        }
412
413
        return $wkb;
414
    }
415
416
    /**
417
     * @param MultiPoint|MultiPolygon|MultiLineString|GeometryCollection $geometry
418
     * @return string
419
     */
420
    protected function writeMulti(Geometry $geometry): string
421
    {
422
        // Set the number of components
423
        $wkb = $this->writer->writeUInt32($geometry->numGeometries());
424
425
        // Write the components
426
        foreach ($geometry->getComponents() as $component) {
427
            $wkb .= $this->writeGeometry($component);
428
        }
429
430
        return $wkb;
431
    }
432
433
    /**
434
     * @param Geometry $geometry
435
     * @param bool $writeSRID default false
436
     * @return string
437
     */
438
    protected function writeType(Geometry $geometry, bool $writeSRID = false): string
439
    {
440
        $type = self::$typeMap[$geometry->geometryType()];
441
        
442
        // Binary OR to mix in additional properties
443
        if ($this->hasZ) {
444
            $type = $type | $this::Z_MASK;
445
        }
446
        if ($this->hasM) {
447
            $type = $type | $this::M_MASK;
448
        }
449
        if ($writeSRID && $geometry->getSRID()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $geometry->getSRID() 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...
450
            $type = $type | $this::SRID_MASK;
451
        }
452
        
453
        return $this->writer->writeUInt32($type) .
454
            ($writeSRID && $geometry->getSRID() ? $this->writer->writeUInt32($this->SRID) : '');
0 ignored issues
show
Bug Best Practice introduced by
The expression $geometry->getSRID() 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...
455
    }
456
}
457