1<?php
2
3namespace geoPHP\Adapter;
4
5use geoPHP\Geometry\Collection;
6use geoPHP\geoPHP;
7use geoPHP\Geometry\Geometry;
8use geoPHP\Geometry\GeometryCollection;
9use geoPHP\Geometry\Point;
10use geoPHP\Geometry\MultiPoint;
11use geoPHP\Geometry\LineString;
12use geoPHP\Geometry\MultiLineString;
13use geoPHP\Geometry\Polygon;
14use geoPHP\Geometry\MultiPolygon;
15
16/**
17 * WKT (Well Known Text) Adapter
18 */
19class WKT implements GeoAdapter
20{
21
22    protected $hasZ      = false;
23
24    protected $measured  = false;
25
26    /**
27     * Determines if the given typeString is a valid WKT geometry type
28     *
29     * @param string $typeString Type to find, eg. "Point", or "LineStringZ"
30     * @return string|bool The geometry type if found or false
31     */
32    public static function isWktType($typeString)
33    {
34        foreach (geoPHP::getGeometryList() as $geom => $type) {
35            if (strtolower((substr($typeString, 0, strlen($geom)))) == $geom) {
36                return $type;
37            }
38        }
39        return false;
40    }
41
42    /**
43     * Read WKT string into geometry objects
44     *
45     * @param string $wkt A WKT string
46     * @return Geometry
47     * @throws \Exception
48     */
49    public function read($wkt)
50    {
51        $this->hasZ = false;
52        $this->measured = false;
53
54        $wkt = trim(strtoupper($wkt));
55        $srid = null;
56        // If it contains a ';', then it contains additional SRID data
57        if (preg_match('/^SRID=(\d+);/', $wkt, $m)) {
58            $srid = $m[1];
59            $wkt = substr($wkt, strlen($m[0]));
60        }
61
62        // If geos is installed, then we take a shortcut and let it parse the WKT
63        if (geoPHP::geosInstalled()) {
64            /** @noinspection PhpUndefinedClassInspection */
65            $reader = new \GEOSWKTReader();
66            try {
67                $geom = geoPHP::geosToGeometry($reader->read($wkt));
68                if ($srid) {
69                    $geom->setSRID($srid);
70                }
71                return $geom;
72            } catch (\Exception $e) {
73//                if ($e->getMessage() !== 'IllegalArgumentException: Empty Points cannot be represented in WKB') {
74//                    throw $e;
75//                } // else try with GeoPHP' parser
76            }
77        }
78
79        if ($geometry = $this->parseTypeAndGetData($wkt)) {
80            if ($geometry && $srid) {
81                $geometry->setSRID($srid);
82            }
83            return $geometry;
84        }
85        throw new \Exception('Invalid Wkt');
86    }
87
88    /**
89     * @param string $wkt
90     *
91     * @return Geometry|null
92     * @throws \Exception
93     */
94    private function parseTypeAndGetData($wkt)
95    {
96        // geometry type is the first word
97        if (preg_match('/^(?<type>[A-Z]+)\s*(?<z>Z*)(?<m>M*)\s*(?:\((?<data>.+)\)|(?<data_empty>EMPTY))$/', $wkt, $m)) {
98            $geometryType = $this->isWktType($m['type']);
99            // Not used yet
100            //$this->hasZ   = $this->hasZ || $m['z'];
101            //$this->measured = $this->measured || $m['m'];
102            $dataString = $m['data'] ?: $m['data_empty'];
103
104            if ($geometryType) {
105                $method = 'parse' . $geometryType;
106                return call_user_func([$this, $method], $dataString);
107            }
108            throw new \Exception('Invalid WKT type "' . $m[1] . '"');
109        }
110        throw new \Exception('Cannot parse WKT');
111    }
112
113    private function parsePoint($dataString)
114    {
115        $dataString = trim($dataString);
116        // If it's marked as empty, then return an empty point
117        if ($dataString == 'EMPTY') {
118            return new Point();
119        }
120        $z = $m = null;
121        $parts = explode(' ', $dataString);
122        if (isset($parts[2])) {
123            if ($this->measured) {
124                $m = $parts[2];
125            } else {
126                $z = $parts[2];
127            }
128        }
129        if (isset($parts[3])) {
130            $m = $parts[3];
131        }
132        return new Point($parts[0], $parts[1], $z, $m);
133    }
134
135    private function parseLineString($dataString)
136    {
137        // If it's marked as empty, then return an empty line
138        if ($dataString == 'EMPTY') {
139            return new LineString();
140        }
141
142        $points = [];
143        foreach (explode(',', $dataString) as $part) {
144            $points[] = $this->parsePoint($part);
145        }
146        return new LineString($points);
147    }
148
149    private function parsePolygon($dataString)
150    {
151        // If it's marked as empty, then return an empty polygon
152        if ($dataString == 'EMPTY') {
153            return new Polygon();
154        }
155
156        $lines = [];
157        if (preg_match_all('/\(([^)(]*)\)/', $dataString, $m)) {
158            foreach ($m[1] as $part) {
159                $lines[] = $this->parseLineString($part);
160            }
161        }
162        return new Polygon($lines);
163    }
164
165    /** @noinspection PhpUnusedPrivateMethodInspection
166     * @param string $dataString
167     *
168     * @return MultiPoint
169     */
170    private function parseMultiPoint($dataString)
171    {
172        // If it's marked as empty, then return an empty MultiPoint
173        if ($dataString == 'EMPTY') {
174            return new MultiPoint();
175        }
176
177        $points = [];
178        /* Should understand both forms:
179         * MULTIPOINT ((1 2), (3 4))
180         * MULTIPOINT (1 2, 3 4)
181         */
182        foreach (explode(',', $dataString) as $part) {
183            $points[] =  $this->parsePoint(trim($part, ' ()'));
184        }
185        return new MultiPoint($points);
186    }
187
188    /** @noinspection PhpUnusedPrivateMethodInspection
189     * @param string $dataString
190     *
191     * @return MultiLineString
192     */
193    private function parseMultiLineString($dataString)
194    {
195        // If it's marked as empty, then return an empty multi-linestring
196        if ($dataString == 'EMPTY') {
197            return new MultiLineString();
198        }
199        $lines = [];
200        if (preg_match_all('/(\([^(]+\)|EMPTY)/', $dataString, $m)) {
201            foreach ($m[1] as $part) {
202                $lines[] =  $this->parseLineString(trim($part, ' ()'));
203            }
204        }
205        return new MultiLineString($lines);
206    }
207
208    /** @noinspection PhpUnusedPrivateMethodInspection
209     * @param string $dataString
210     *
211     * @return MultiPolygon
212     */
213    private function parseMultiPolygon($dataString)
214    {
215        // If it's marked as empty, then return an empty multi-polygon
216        if ($dataString == 'EMPTY') {
217            return new MultiPolygon();
218        }
219
220        $polygons = [];
221        if (preg_match_all('/(\(\([^(].+\)\)|EMPTY)/', $dataString, $m)) {
222            foreach ($m[0] as $part) {
223                $polygons[] =  $this->parsePolygon($part);
224            }
225        }
226        return new MultiPolygon($polygons);
227    }
228
229    /** @noinspection PhpUnusedPrivateMethodInspection
230     * @param string $dataString
231     *
232     * @return GeometryCollection
233     */
234    private function parseGeometryCollection($dataString)
235    {
236        // If it's marked as empty, then return an empty geom-collection
237        if ($dataString == 'EMPTY') {
238            return new GeometryCollection();
239        }
240
241        $geometries = [];
242        while (strlen($dataString) > 0) {
243            // Matches the first balanced parenthesis group (or term EMPTY)
244            preg_match(
245                '/\((?>[^()]+|(?R))*\)|EMPTY/',
246                $dataString,
247                $m,
248                PREG_OFFSET_CAPTURE
249            );
250            if (!isset($m[0])) {
251                // something weird happened, we stop here before running in an infinite loop
252                break;
253            }
254            $cutPosition = strlen($m[0][0]) + $m[0][1];
255            $geometry = $this->parseTypeAndGetData(trim(substr($dataString, 0, $cutPosition)));
256            $geometries[] = $geometry;
257            $dataString = trim(substr($dataString, $cutPosition + 1));
258        }
259
260        return new GeometryCollection($geometries);
261    }
262
263
264    /**
265     * Serialize geometries into a WKT string.
266     *
267     * @param Geometry $geometry
268     *
269     * @return string The WKT string representation of the input geometries
270     */
271    public function write(Geometry $geometry)
272    {
273        // If geos is installed, then we take a shortcut and let it write the WKT
274        if (geoPHP::geosInstalled()) {
275            /** @noinspection PhpUndefinedClassInspection */
276            $writer = new \GEOSWKTWriter();
277            /** @noinspection PhpUndefinedMethodInspection */
278            $writer->setRoundingPrecision(14);
279            /** @noinspection PhpUndefinedMethodInspection */
280            $writer->setTrim(true);
281            /** @noinspection PhpUndefinedMethodInspection */
282            return $writer->write($geometry->getGeos());
283        }
284        $this->measured = $geometry->isMeasured();
285        $this->hasZ     = $geometry->hasZ();
286
287        if ($geometry->isEmpty()) {
288            return strtoupper($geometry->geometryType()) . ' EMPTY';
289        }
290
291        if ($data = $this->extractData($geometry)) {
292            $extension = '';
293            if ($this->hasZ) {
294                $extension .= 'Z';
295            }
296            if ($this->measured) {
297                $extension .= 'M';
298            }
299            return strtoupper($geometry->geometryType()) . ($extension ? ' ' . $extension : '') . ' (' . $data . ')';
300        }
301        return '';
302    }
303
304    /**
305     * Extract geometry to a WKT string
306     *
307     * @param Geometry|Collection $geometry A Geometry object
308     *
309     * @return string
310     */
311    public function extractData($geometry)
312    {
313        $parts = [];
314        switch ($geometry->geometryType()) {
315            case Geometry::POINT:
316                $p = $geometry->x() . ' ' . $geometry->y();
317                if ($geometry->hasZ()) {
318                    $p .= ' ' . $geometry->getZ();
319                    $this->hasZ = $this->hasZ || $geometry->hasZ();
320                }
321                if ($geometry->isMeasured()) {
322                    $p .= ' ' . $geometry->getM();
323                    $this->measured = $this->measured || $geometry->isMeasured();
324                }
325                return $p;
326            case Geometry::LINE_STRING:
327                foreach ($geometry->getComponents() as $component) {
328                    $parts[] = $this->extractData($component);
329                }
330                return implode(', ', $parts);
331            case Geometry::POLYGON:
332            case Geometry::MULTI_POINT:
333            case Geometry::MULTI_LINE_STRING:
334            case Geometry::MULTI_POLYGON:
335                foreach ($geometry->getComponents() as $component) {
336                    if ($component->isEmpty()) {
337                        $parts[] = 'EMPTY';
338                    } else {
339                        $parts[] = '(' . $this->extractData($component) . ')';
340                    }
341                }
342                return implode(', ', $parts);
343            case Geometry::GEOMETRY_COLLECTION:
344                foreach ($geometry->getComponents() as $component) {
345                    $this->hasZ = $this->hasZ || $geometry->hasZ();
346                    $this->measured = $this->measured || $geometry->isMeasured();
347
348                    $extension = '';
349                    if ($this->hasZ) {
350                        $extension .= 'Z';
351                    }
352                    if ($this->measured) {
353                        $extension .= 'M';
354                    }
355                    $data = $this->extractData($component);
356                    $parts[] = strtoupper($component->geometryType())
357                            . ($extension ? ' ' . $extension : '')
358                            . ($data ? ' (' . $data . ')' : ' EMPTY');
359                }
360                return implode(', ', $parts);
361        }
362        return '';
363    }
364}
365