1<?php
2
3namespace geoPHP\Adapter;
4
5use geoPHP\geoPHP;
6use geoPHP\Geometry\Geometry;
7use geoPHP\Geometry\GeometryCollection;
8use geoPHP\Geometry\Point;
9use geoPHP\Geometry\MultiPoint;
10use geoPHP\Geometry\LineString;
11use geoPHP\Geometry\MultiLineString;
12use geoPHP\Geometry\Polygon;
13use geoPHP\Geometry\MultiPolygon;
14
15/**
16 * GeoJSON class : a geoJSON reader/writer.
17 *
18 * Note that it will always return a GeoJSON geometry. This
19 * means that if you pass it a feature, it will return the
20 * geometry of that feature strip everything else.
21 */
22class GeoJSON implements GeoAdapter
23{
24    /**
25     * Given an object or a string, return a Geometry
26     *
27     * @param string|object $input The GeoJSON string or object
28     * @return Geometry
29     * @throws \Exception
30     */
31    public function read($input)
32    {
33        if (is_string($input)) {
34            $input = json_decode($input);
35        }
36        if (!is_object($input)) {
37            throw new \Exception('Invalid JSON');
38        }
39        if (!isset($input->type) || !is_string($input->type)) {
40            throw new \Exception('Invalid GeoJSON');
41        }
42
43        // Check to see if it's a FeatureCollection
44        if ($input->type == 'FeatureCollection' && isset($input->features)) {
45            $geometries = [];
46            foreach ($input->features as $feature) {
47                $geometries[] = $this->read($feature);
48            }
49            return geoPHP::buildGeometry($geometries);
50        }
51
52        // Check to see if it's a Feature
53        if ($input->type == 'Feature') {
54            return $this->geoJSONFeatureToGeometry($input);
55        }
56
57        // It's a geometry - process it
58        return $this->geoJSONObjectToGeometry($input);
59    }
60
61    /**
62     * @param object $input
63     * @return string|null
64     */
65    private function getSRID($input)
66    {
67        if (isset($input->crs->properties->name)) {
68            // parse CRS codes in forms "EPSG:1234" and "urn:ogc:def:crs:EPSG::1234"
69            preg_match('#EPSG[:]+(\d+)#', $input->crs->properties->name, $m);
70            return isset($m[1]) ? $m[1] : null;
71        }
72        return null;
73    }
74
75    /**
76     * @param object $obj
77     * @return Geometry
78     * @throws \Exception
79     */
80    private function geoJSONFeatureToGeometry($obj)
81    {
82        $geometry = $this->read($obj->geometry);
83        if (isset($obj->properties)) {
84            foreach ($obj->properties as $property => $value) {
85                $geometry->setData($property, $value);
86            }
87        }
88
89        return $geometry;
90    }
91
92    /**
93     * @param object $obj
94     * @return Geometry
95     * @throws \Exception
96     */
97    private function geoJSONObjectToGeometry($obj)
98    {
99        $type = $obj->type;
100
101        if ($type == 'GeometryCollection') {
102            return $this->geoJSONObjectToGeometryCollection($obj);
103        }
104        $method = 'arrayTo' . $type;
105        /** @var GeometryCollection $geometry */
106        $geometry = $this->$method($obj->coordinates);
107        $geometry->setSRID($this->getSRID($obj));
108        return $geometry;
109    }
110
111    /**
112     * @param array $coordinates Array of coordinates
113     * @return Point
114     */
115    private function arrayToPoint($coordinates)
116    {
117        switch (count($coordinates)) {
118            case 2:
119                return new Point($coordinates[0], $coordinates[1]);
120                break;
121            case 3:
122                return new Point($coordinates[0], $coordinates[1], $coordinates[2]);
123                break;
124            case 4:
125                return new Point($coordinates[0], $coordinates[1], $coordinates[2], $coordinates[3]);
126                break;
127            default:
128                return new Point();
129        }
130    }
131
132    private function arrayToLineString($array)
133    {
134        $points = [];
135        foreach ($array as $componentArray) {
136            $points[] = $this->arrayToPoint($componentArray);
137        }
138        return new LineString($points);
139    }
140
141    private function arrayToPolygon($array)
142    {
143        $lines = [];
144        foreach ($array as $componentArray) {
145            $lines[] = $this->arrayToLineString($componentArray);
146        }
147        return new Polygon($lines);
148    }
149
150    /** @noinspection PhpUnusedPrivateMethodInspection */
151    /**
152     * @param array $array
153     * @return MultiPoint
154     */
155    private function arrayToMultiPoint($array)
156    {
157        $points = [];
158        foreach ($array as $componentArray) {
159            $points[] = $this->arrayToPoint($componentArray);
160        }
161        return new MultiPoint($points);
162    }
163
164    /** @noinspection PhpUnusedPrivateMethodInspection */
165    /**
166     * @param array $array
167     * @return MultiLineString
168     */
169    private function arrayToMultiLineString($array)
170    {
171        $lines = [];
172        foreach ($array as $componentArray) {
173            $lines[] = $this->arrayToLineString($componentArray);
174        }
175        return new MultiLineString($lines);
176    }
177
178    /** @noinspection PhpUnusedPrivateMethodInspection */
179    /**
180     * @param array $array
181     * @return MultiPolygon
182     */
183    private function arrayToMultiPolygon($array)
184    {
185        $polygons = [];
186        foreach ($array as $componentArray) {
187            $polygons[] = $this->arrayToPolygon($componentArray);
188        }
189        return new MultiPolygon($polygons);
190    }
191
192    /**
193     * @param object $obj
194     * @throws \Exception
195     * @return GeometryCollection
196     */
197    private function geoJSONObjectToGeometryCollection($obj)
198    {
199        $geometries = [];
200        if (!property_exists($obj, 'geometries')) {
201            throw new \Exception('Invalid GeoJSON: GeometryCollection with no component geometries');
202        }
203        foreach ($obj->geometries ?: [] as $componentObject) {
204            $geometries[] = $this->geoJSONObjectToGeometry($componentObject);
205        }
206        $collection = new GeometryCollection($geometries);
207        $collection->setSRID($this->getSRID($obj));
208        return $collection;
209    }
210
211    /**
212     * Serializes an object into a geojson string
213     *
214     *
215     * @param Geometry $geometry The object to serialize
216     * @param boolean  $returnAsArray
217     *
218     * @return string|array The GeoJSON string
219     */
220    public function write(Geometry $geometry, $returnAsArray = false)
221    {
222        return $returnAsArray
223            ? $this->getArray($geometry)
224            : json_encode($this->getArray($geometry));
225    }
226
227
228
229    /**
230     * Creates a geoJSON array
231     *
232     * If the root geometry is a GeometryCollection, and any of its geometries has data,
233     * the root element will be a FeatureCollection with Feature elements (with the data)
234     * If the root geometry has data, it will be included in a Feature object that contains the data
235     *
236     * The geometry should have geographical coordinates since CRS support has been removed from from geoJSON specification (RFC 7946)
237     * The geometry should'nt be measured, since geoJSON specification (RFC 7946) only supports the dimensional positions
238     *
239     * @param Geometry|GeometryCollection $geometry
240     * @param bool|null $isRoot Is geometry the root geometry?
241     * @return array
242     */
243    public function getArray($geometry, $isRoot = true)
244    {
245        if ($geometry->geometryType() === Geometry::GEOMETRY_COLLECTION) {
246            $components = [];
247            $isFeatureCollection = false;
248            foreach ($geometry->getComponents() as $component) {
249                if ($component->getData() !== null) {
250                    $isFeatureCollection = true;
251                }
252                $components[] = $this->getArray($component, false);
253            }
254            if (!$isFeatureCollection || !$isRoot) {
255                return [
256                        'type'       => 'GeometryCollection',
257                        'geometries' => $components
258                ];
259            } else {
260                $features = [];
261                foreach ($geometry->getComponents() as $i => $component) {
262                    $features[] = [
263                            'type'       => 'Feature',
264                            'properties' => $component->getData(),
265                            'geometry'   => $components[$i],
266                    ];
267                }
268                return [
269                        'type'     => 'FeatureCollection',
270                        'features' => $features
271                ];
272            }
273        }
274
275        if ($isRoot && $geometry->getData() !== null) {
276            return [
277                    'type'       => 'Feature',
278                    'properties' => $geometry->getData(),
279                    'geometry'   => [
280                            'type'        => $geometry->geometryType(),
281                            'coordinates' => $geometry->isEmpty() ? [] : $geometry->asArray()
282                    ]
283            ];
284        }
285        $object = [
286                'type'        => $geometry->geometryType(),
287                'coordinates' => $geometry->isEmpty() ? [] : $geometry->asArray()
288        ];
289        return $object;
290    }
291}
292