TWKBWriter::writeLineString()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

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