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\LineString;
11use geoPHP\Geometry\Polygon;
12
13/*
14 * Copyright (c) Patrick Hayes
15 *
16 * This code is open-source and licenced under the Modified BSD License.
17 * For the full copyright and license information, please view the LICENSE
18 * file that was distributed with this source code.
19 */
20
21/**
22 * PHP Geometry/GeoRSS encoder/decoder
23 */
24class GeoRSS implements GeoAdapter
25{
26
27    /**
28     * @var \DOMDocument $xmlObject
29     */
30    protected $xmlObject;
31
32    private $namespace = false;
33
34    private $nss = ''; // Name-space string. eg 'georss:'
35
36    /**
37     * Read GeoRSS string into geometry objects
38     *
39     * @param string $georss - an XML feed containing geoRSS
40     *
41     * @return Geometry|GeometryCollection
42     */
43    public function read($georss)
44    {
45        return $this->geomFromText($georss);
46    }
47
48    /**
49     * Serialize geometries into a GeoRSS string.
50     *
51     * @param Geometry $geometry
52     * @param boolean|string $namespace
53     * @return string The georss string representation of the input geometries
54     */
55    public function write(Geometry $geometry, $namespace = false)
56    {
57        if ($namespace) {
58            $this->namespace = $namespace;
59            $this->nss = $namespace . ':';
60        }
61        return $this->geometryToGeoRSS($geometry) ?: '';
62    }
63
64    public function geomFromText($text)
65    {
66        // Change to lower-case, strip all CDATA, and de-namespace
67        $text = strtolower($text);
68        $text = preg_replace('/<!\[cdata\[(.*?)\]\]>/s', '', $text);
69
70        // Load into DOMDocument
71        $xmlObject = new \DOMDocument();
72        @$xmlObject->loadXML($text);
73        if ($xmlObject === false) {
74            throw new \Exception("Invalid GeoRSS: " . $text);
75        }
76
77        $this->xmlObject = $xmlObject;
78        try {
79            $geom = $this->geomFromXML();
80        } catch (\Exception $e) {
81            throw new \Exception("Cannot Read Geometry From GeoRSS: " . $e->getMessage());
82        }
83
84        return $geom;
85    }
86
87    protected function geomFromXML()
88    {
89        $geometries = [];
90        $geometries = array_merge($geometries, $this->parsePoints());
91        $geometries = array_merge($geometries, $this->parseLines());
92        $geometries = array_merge($geometries, $this->parsePolygons());
93        $geometries = array_merge($geometries, $this->parseBoxes());
94        $geometries = array_merge($geometries, $this->parseCircles());
95
96        if (empty($geometries)) {
97            throw new \Exception("Invalid / Empty GeoRSS");
98        }
99
100        return geoPHP::geometryReduce($geometries);
101    }
102
103    protected function getPointsFromCoordinates($string)
104    {
105        $coordinates = [];
106        $latitudeAndLongitude = explode(' ', $string);
107        $lat = 0;
108        foreach ($latitudeAndLongitude as $key => $item) {
109            if (!($key % 2)) {
110                // It's a latitude
111                $lat = is_numeric($item) ? $item : NAN;
112            } else {
113                // It's a longitude
114                $lon = is_numeric($item) ? $item : NAN;
115                $coordinates[] = new Point($lon, $lat);
116            }
117        }
118        return $coordinates;
119    }
120
121    protected function parsePoints()
122    {
123        $points = [];
124        $pointElements = $this->xmlObject->getElementsByTagName('point');
125        foreach ($pointElements as $pt) {
126            $pointArray = $this->getPointsFromCoordinates(trim($pt->firstChild->nodeValue));
127            $points[] = !empty($pointArray) ? $pointArray[0] : new Point();
128        }
129        return $points;
130    }
131
132    protected function parseLines()
133    {
134        $lines = [];
135        $lineElements = $this->xmlObject->getElementsByTagName('line');
136        foreach ($lineElements as $line) {
137            $components = $this->getPointsFromCoordinates(trim($line->firstChild->nodeValue));
138            $lines[] = new LineString($components);
139        }
140        return $lines;
141    }
142
143    protected function parsePolygons()
144    {
145        $polygons = [];
146        $polygonElements = $this->xmlObject->getElementsByTagName('polygon');
147        foreach ($polygonElements as $polygon) {
148            /** @noinspection PhpUndefinedMethodInspection */
149            if ($polygon->hasChildNodes()) {
150                $points = $this->getPointsFromCoordinates(trim($polygon->firstChild->nodeValue));
151                $exteriorRing = new LineString($points);
152                $polygons[] = new Polygon([$exteriorRing]);
153            } else {
154                // It's an EMPTY polygon
155                $polygons[] = new Polygon();
156            }
157        }
158        return $polygons;
159    }
160
161    // Boxes are rendered into polygons
162    protected function parseBoxes()
163    {
164        $polygons = [];
165        $boxElements = $this->xmlObject->getElementsByTagName('box');
166        foreach ($boxElements as $box) {
167            $parts = explode(' ', trim($box->firstChild->nodeValue));
168            $components = [
169                    new Point($parts[3], $parts[2]),
170                    new Point($parts[3], $parts[0]),
171                    new Point($parts[1], $parts[0]),
172                    new Point($parts[1], $parts[2]),
173                    new Point($parts[3], $parts[2]),
174            ];
175            $exteriorRing = new LineString($components);
176            $polygons[] = new Polygon([$exteriorRing]);
177        }
178        return $polygons;
179    }
180
181    // Circles are rendered into points
182    // @@TODO: Add good support once we have circular-string geometry support
183    protected function parseCircles()
184    {
185        $points = [];
186        $circleElements = $this->xmlObject->getElementsByTagName('circle');
187        foreach ($circleElements as $circle) {
188            $parts = explode(' ', trim($circle->firstChild->nodeValue));
189            $points[] = new Point($parts[1], $parts[0]);
190        }
191        return $points;
192    }
193
194    /**
195     * @param Geometry $geometry
196     * @return string|null
197     */
198    protected function geometryToGeoRSS($geometry)
199    {
200        $type = $geometry->geometryType();
201        switch ($type) {
202            case Geometry::POINT:
203                return $this->pointToGeoRSS($geometry);
204            case Geometry::LINE_STRING:
205                /** @noinspection PhpParamsInspection */
206                return $this->linestringToGeoRSS($geometry);
207            case Geometry::POLYGON:
208                /** @noinspection PhpParamsInspection */
209                return $this->PolygonToGeoRSS($geometry);
210            case Geometry::MULTI_POINT:
211            case Geometry::MULTI_LINE_STRING:
212            case Geometry::MULTI_POLYGON:
213            case Geometry::GEOMETRY_COLLECTION:
214            /** @noinspection PhpParamsInspection */
215                return $this->collectionToGeoRSS($geometry);
216        }
217        return null;
218    }
219
220    /**
221     * @param Geometry $geometry
222     * @return string
223     */
224    private function pointToGeoRSS($geometry)
225    {
226        return '<' . $this->nss . 'point>' . $geometry->y() . ' ' . $geometry->x() . '</' . $this->nss . 'point>';
227    }
228
229    /**
230     * @param LineString $geometry
231     * @return string
232     */
233    private function linestringToGeoRSS($geometry)
234    {
235        $output = '<' . $this->nss . 'line>';
236        foreach ($geometry->getComponents() as $k => $point) {
237            $output .= $point->y() . ' ' . $point->x();
238            if ($k < ($geometry->numGeometries() - 1)) {
239                $output .= ' ';
240            }
241        }
242        $output .= '</' . $this->nss . 'line>';
243        return $output;
244    }
245
246    /**
247     * @param Polygon $geometry
248     * @return string
249     */
250    private function polygonToGeoRSS($geometry)
251    {
252        $output = '<' . $this->nss . 'polygon>';
253        $exteriorRing = $geometry->exteriorRing();
254        foreach ($exteriorRing->getComponents() as $k => $point) {
255            $output .= $point->y() . ' ' . $point->x();
256            if ($k < ($exteriorRing->numGeometries() - 1)) {
257                $output .= ' ';
258            }
259        }
260        $output .= '</' . $this->nss . 'polygon>';
261        return $output;
262    }
263
264    /**
265     * @param Collection $geometry
266     * @return string
267     */
268    public function collectionToGeoRSS($geometry)
269    {
270        $georss = '<' . $this->nss . 'where>';
271        $components = $geometry->getComponents();
272        foreach ($components as $component) {
273            $georss .= $this->geometryToGeoRSS($component);
274        }
275
276        $georss .= '</' . $this->nss . 'where>';
277
278        return $georss;
279    }
280}
281