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

TWKBWriter::writePolygon()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 8
rs 10
cc 2
nc 2
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\Collection;
16
use geoPHP\Geometry\Geometry;
17
use geoPHP\Geometry\Point;
18
use geoPHP\Geometry\LineString;
19
use geoPHP\Geometry\Polygon;
20
21
/**
22
 * PHP Geometry <-> TWKB encoder/decoder
23
 *
24
 * "Tiny Well-known Binary is is a multi-purpose format for serializing vector geometry data into a byte buffer,
25
 * with an emphasis on minimizing size of the buffer."
26
 * @see https://github.com/TWKB/Specification/blob/master/twkb.md
27
 *
28
 * This implementation supports:
29
 * - reading and writing all geometry types (1-7)
30
 * - empty geometries
31
 * - extended precision (Z, M coordinates; custom precision)
32
 * Partially supports:
33
 * - bounding box: can read and write, but don't store readed boxes (API missing)
34
 * - size attribute: can read and write size attribute, but seeking is not supported
35
 * - ID list: can read and write, but API is completely missing
36
 */
37
trait TWKBWriter
38
{
39
40
    /**
41
     * @var array{'decimalDigitsXY':int,'decimalDigitsZ':int,'decimalDigitsM':int,'includeSize':bool,'includeBoundingBoxes':bool,'hasM':bool,'hasZ':bool,'xyFactor':int|float,'zFactor':int|float,'mFactor':int|float}
42
     */
43
    protected $writeOptions = [
44
        'decimalDigitsXY' => 5,
45
        'decimalDigitsZ' => 0,
46
        'decimalDigitsM' => 0,
47
        'includeSize' => false,
48
        'includeBoundingBoxes' => false,
49
        'hasM' => false,
50
        'hasZ' => false,
51
        'xyFactor' => 1,
52
        'zFactor' => 1,
53
        'mFactor' => 1
54
    ];
55
56
    /**
57
     * @var BinaryWriter
58
     */
59
    protected $writer;
60
61
    /**
62
     * Serialize geometries into TWKB string.
63
     *
64
     * @return string The WKB string representation of the input geometries
65
     * @param Geometry $geometry The geometry
66
     * @param bool|true $writeAsHex Write the result in binary or hexadecimal system
67
     * @param int $decimalDigitsXY Coordinate precision of X and Y. Default is 5 decimals
68
     * @param int $decimalDigitsZ Coordinate precision of Z. Default is 0 decimal
69
     * @param int $decimalDigitsM Coordinate precision of M. Default is 0 decimal
70
     * @param bool $includeSizes Includes the size in bytes of the remainder of the geometry after the size attribute. Default is false
71
     * @param bool $includeBoundingBoxes Includes the coordinates of bounding box' two corner. Default is false
72
     *
73
     * @return string binary or hexadecimal representation of TWKB
74
     */
75
    public function write(
76
        Geometry $geometry,
77
        bool $writeAsHex = false,
78
        $decimalDigitsXY = null,
79
        $decimalDigitsZ = null,
80
        $decimalDigitsM = null,
81
        bool $includeSizes = false,
82
        bool $includeBoundingBoxes = false
83
    ): string {
84
        $this->writer = new BinaryWriter();
85
86
        $this->writeOptions['decimalDigitsXY'] = is_numeric($decimalDigitsXY) ? (int) $decimalDigitsXY : $this->writeOptions['decimalDigitsXY'];
87
        $this->writeOptions['decimalDigitsZ'] = is_numeric($decimalDigitsZ) ? (int) $decimalDigitsZ : $this->writeOptions['decimalDigitsZ'];
88
        $this->writeOptions['decimalDigitsM'] = is_numeric($decimalDigitsM) ? (int) $decimalDigitsM : $this->writeOptions['decimalDigitsM'];
89
        $this->writeOptions['includeSize'] = $includeSizes ? true : $this->writeOptions['includeSize'];
90
        $this->writeOptions['includeBoundingBoxes'] = $includeBoundingBoxes ? true : $this->writeOptions['includeBoundingBoxes'];
91
        $this->writeOptions['xyFactor'] = pow(10, $this->writeOptions['decimalDigitsXY']);
92
        $this->writeOptions['zFactor'] = pow(10, $this->writeOptions['decimalDigitsZ']);
93
        $this->writeOptions['mFactor'] = pow(10, $this->writeOptions['decimalDigitsM']);
94
        
95
        $twkb = $this->writeGeometry($geometry);
96
        
97
        return $writeAsHex ? current((array) unpack('H*', $twkb)) : $twkb;
98
    }
99
100
    /**
101
     * @param Geometry $geometry
102
     * @return string
103
     */
104
    protected function writeGeometry(Geometry $geometry): string
105
    {
106
        $this->writeOptions['hasZ'] = $geometry->hasZ();
107
        $this->writeOptions['hasM'] = $geometry->isMeasured();
108
109
        // Type and precision
110
        $type = self::$typeMap[$geometry->geometryType()] +
111
            (BinaryWriter::zigZagEncode($this->writeOptions['decimalDigitsXY']) << 4);
112
        $twkbHead = $this->writer->writeUInt8($type);
113
114
        // Is there extended precision information?
115
        $metadataHeader = $this->writeOptions['includeBoundingBoxes'] << 0;
116
        // Is there extended precision information?
117
        $metadataHeader += $this->writeOptions['includeSize'] << 1;
118
        // Is there an ID list?
119
        // TODO: implement this (needs metadata support in geoPHP)
120
        //$metadataHeader += $this->writeOptions['hasIdList'] << 2;
121
        // Is there extended precision information?
122
        $metadataHeader += ($geometry->hasZ() || $geometry->isMeasured()) << 3;
123
        // Is this an empty geometry?
124
        $metadataHeader += $geometry->isEmpty() << 4;
125
126
        $twkbHead .= $this->writer->writeUInt8($metadataHeader);
127
128
        $twkbGeom = '';
129
        if (!$geometry->isEmpty()) {
130
            $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...
131
132
            switch ($geometry->geometryType()) {
133
                case Geometry::POINT:
134
                    /** @var Point $geometry */
135
                    $twkbGeom .= $this->writePoint($geometry);
136
                    break;
137
                case Geometry::LINESTRING:
138
                    /** @var LineString $geometry */
139
                    $twkbGeom .= $this->writeLineString($geometry);
140
                    break;
141
                case Geometry::POLYGON:
142
                    /** @var Polygon $geometry */
143
                    $twkbGeom .= $this->writePolygon($geometry);
144
                    break;
145
                case Geometry::MULTI_POINT:
146
                case Geometry::MULTI_LINESTRING:
147
                case Geometry::MULTI_POLYGON:
148
                case Geometry::GEOMETRY_COLLECTION:
149
                    /** @var Collection $geometry */
150
                    $twkbGeom .= $this->writeMulti($geometry);
151
                    break;
152
            }
153
        }
154
155
        if ($this->writeOptions['includeBoundingBoxes']) {
156
            $bBox = $geometry->getBoundingBox();
157
            
158
            if (!empty($bBox)) {
159
                // X
160
                $twkbBox = $this->writer->writeSVarInt($bBox['minx'] * $this->writeOptions['xyFactor']);
161
                $twkbBox .= $this->writer->writeSVarInt(($bBox['maxx'] - $bBox['minx']) * $this->writeOptions['xyFactor']);
162
                // Y
163
                $twkbBox .= $this->writer->writeSVarInt($bBox['miny'] * $this->writeOptions['xyFactor']);
164
                $twkbBox .= $this->writer->writeSVarInt(($bBox['maxy'] - $bBox['miny']) * $this->writeOptions['xyFactor']);
165
                if ($geometry->hasZ()) {
166
                    $bBox['minz'] = $geometry->minimumZ();
167
                    $bBox['maxz'] = $geometry->maximumZ();
168
                    $twkbBox .= $this->writer->writeSVarInt(round($bBox['minz'] * $this->writeOptions['zFactor']));
169
                    $twkbBox .= $this->writer->writeSVarInt(round(($bBox['maxz'] - $bBox['minz']) * $this->writeOptions['zFactor']));
170
                }
171
                if ($geometry->isMeasured()) {
172
                    $bBox['minm'] = $geometry->minimumM();
173
                    $bBox['maxm'] = $geometry->maximumM();
174
                    $twkbBox .= $this->writer->writeSVarInt($bBox['minm'] * $this->writeOptions['mFactor']);
175
                    $twkbBox .= $this->writer->writeSVarInt(($bBox['maxm'] - $bBox['minm']) * $this->writeOptions['mFactor']);
176
                }
177
                $twkbGeom = $twkbBox . $twkbGeom;
178
            }
179
        }
180
181
        if ($geometry->hasZ() || $geometry->isMeasured()) {
182
            $extendedPrecision = 0;
183
            if ($geometry->hasZ()) {
184
                $extendedPrecision |= ($geometry->hasZ() ? 0x1 : 0) | ($this->writeOptions['decimalDigitsZ'] << 2);
185
            }
186
            if ($geometry->isMeasured()) {
187
                $extendedPrecision |= ($geometry->isMeasured() ? 0x2 : 0) | ($this->writeOptions['decimalDigitsM'] << 5);
188
            }
189
            $twkbHead .= $this->writer->writeUInt8($extendedPrecision);
190
        }
191
        if ($this->writeOptions['includeSize']) {
192
            $twkbHead .= $this->writer->writeUVarInt(strlen($twkbGeom));
193
        }
194
195
        return $twkbHead . $twkbGeom;
196
    }
197
198
    /**
199
     * @param Point $geometry
200
     * @return string
201
     */
202
    protected function writePoint(Point $geometry): string
203
    {
204
        $x = round($geometry->getX() * $this->writeOptions['xyFactor']);
205
        $y = round($geometry->getY() * $this->writeOptions['xyFactor']);
206
        $z = round($geometry->getZ() * $this->writeOptions['zFactor']);
207
        $m = round($geometry->m() * $this->writeOptions['mFactor']);
208
209
        $twkb = $this->writer->writeSVarInt($x - $this->lastPoint->getX());
210
        $twkb .= $this->writer->writeSVarInt($y - $this->lastPoint->getY());
211
        if ($this->writeOptions['hasZ']) {
212
            $twkb .= $this->writer->writeSVarInt($z - $this->lastPoint->getZ());
213
        }
214
        if ($this->writeOptions['hasM']) {
215
            $twkb .= $this->writer->writeSVarInt($m - $this->lastPoint->m());
216
        }
217
218
        $this->lastPoint = new Point(
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...
219
            $x,
220
            $y,
221
            $this->writeOptions['hasZ'] ? $z : null,
222
            $this->writeOptions['hasM'] ? $m : null
223
        );
224
225
        return $twkb;
226
    }
227
228
    /**
229
     * @param LineString $geometry
230
     * @return string
231
     */
232
    protected function writeLineString(LineString $geometry): string
233
    {
234
        $twkb = $this->writer->writeUVarInt($geometry->numPoints());
235
        foreach ($geometry->getComponents() as $component) {
236
            $twkb .= $this->writePoint($component);
237
        }
238
        return $twkb;
239
    }
240
241
    /**
242
     * @param Polygon $geometry
243
     * @return string
244
     */
245
    protected function writePolygon(Polygon $geometry): string
246
    {
247
        $twkb = $this->writer->writeUVarInt($geometry->numGeometries());
248
        foreach ($geometry->getComponents() as $component) {
249
            $twkb .= $this->writeLineString($component);
250
        }
251
        
252
        return $twkb;
253
    }
254
255
    /**
256
     * @param Collection $geometry
257
     * @return string
258
     */
259
    protected function writeMulti(Collection $geometry): string
260
    {
261
        $twkb = $this->writer->writeUVarInt($geometry->numGeometries());
262
        //if ($geometry->hasIdList()) {
263
        //  foreach ($geometry->getComponents() as $component) {
264
        //      $this->writer->writeUVarInt($component->getId());
265
        //  }
266
        //}
267
        foreach ($geometry->getComponents() as $component) {
268
            if ($geometry->geometryType() !== Geometry::GEOMETRY_COLLECTION) {
269
                $func = 'write' . $component->geometryType();
270
                $twkb .= $this->$func($component);
271
            } else {
272
                $twkb .= $this->writeGeometry($component);
273
            }
274
        }
275
        
276
        return $twkb;
277
    }
278
}
279