1<?php
2
3namespace geoPHP\Adapter;
4
5use geoPHP\Geometry\Geometry;
6use geoPHP\Geometry\GeometryCollection;
7use geoPHP\Geometry\Point;
8use geoPHP\Geometry\MultiPoint;
9use geoPHP\Geometry\LineString;
10use geoPHP\Geometry\MultiLineString;
11use geoPHP\Geometry\Polygon;
12use 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 */
27class 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) {
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) {
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) {
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