1<?php
2/**
3 * Handles actions related to GIS GEOMETRYCOLLECTION objects
4 */
5
6declare(strict_types=1);
7
8namespace PhpMyAdmin\Gis;
9
10use TCPDF;
11use function array_merge;
12use function count;
13use function mb_strlen;
14use function mb_strpos;
15use function mb_substr;
16use function str_split;
17
18/**
19 * Handles actions related to GIS GEOMETRYCOLLECTION objects
20 */
21class GisGeometryCollection extends GisGeometry
22{
23    /** @var self */
24    private static $instance;
25
26    /**
27     * A private constructor; prevents direct creation of object.
28     *
29     * @access private
30     */
31    private function __construct()
32    {
33    }
34
35    /**
36     * Returns the singleton.
37     *
38     * @return GisGeometryCollection the singleton
39     *
40     * @access public
41     */
42    public static function singleton()
43    {
44        if (! isset(self::$instance)) {
45            self::$instance = new GisGeometryCollection();
46        }
47
48        return self::$instance;
49    }
50
51    /**
52     * Scales each row.
53     *
54     * @param string $spatial spatial data of a row
55     *
56     * @return array array containing the min, max values for x and y coordinates
57     *
58     * @access public
59     */
60    public function scaleRow($spatial)
61    {
62        $min_max = [];
63
64        // Trim to remove leading 'GEOMETRYCOLLECTION(' and trailing ')'
65        $goem_col
66            = mb_substr(
67                $spatial,
68                19,
69                mb_strlen($spatial) - 20
70            );
71
72        // Split the geometry collection object to get its constituents.
73        $sub_parts = $this->explodeGeomCol($goem_col);
74
75        foreach ($sub_parts as $sub_part) {
76            $type_pos = mb_strpos($sub_part, '(');
77            if ($type_pos === false) {
78                continue;
79            }
80            $type = mb_substr($sub_part, 0, $type_pos);
81
82            $gis_obj = GisFactory::factory($type);
83            if (! $gis_obj) {
84                continue;
85            }
86            $scale_data = $gis_obj->scaleRow($sub_part);
87
88            // Update minimum/maximum values for x and y coordinates.
89            $c_maxX = (float) $scale_data['maxX'];
90            if (! isset($min_max['maxX']) || $c_maxX > $min_max['maxX']) {
91                $min_max['maxX'] = $c_maxX;
92            }
93
94            $c_minX = (float) $scale_data['minX'];
95            if (! isset($min_max['minX']) || $c_minX < $min_max['minX']) {
96                $min_max['minX'] = $c_minX;
97            }
98
99            $c_maxY = (float) $scale_data['maxY'];
100            if (! isset($min_max['maxY']) || $c_maxY > $min_max['maxY']) {
101                $min_max['maxY'] = $c_maxY;
102            }
103
104            $c_minY = (float) $scale_data['minY'];
105            if (isset($min_max['minY']) && $c_minY >= $min_max['minY']) {
106                continue;
107            }
108
109            $min_max['minY'] = $c_minY;
110        }
111
112        return $min_max;
113    }
114
115    /**
116     * Adds to the PNG image object, the data related to a row in the GIS dataset.
117     *
118     * @param string      $spatial    GIS POLYGON object
119     * @param string|null $label      Label for the GIS POLYGON object
120     * @param string      $color      Color for the GIS POLYGON object
121     * @param array       $scale_data Array containing data related to scaling
122     * @param resource    $image      Image object
123     *
124     * @return resource the modified image object
125     *
126     * @access public
127     */
128    public function prepareRowAsPng($spatial, ?string $label, $color, array $scale_data, $image)
129    {
130        // Trim to remove leading 'GEOMETRYCOLLECTION(' and trailing ')'
131        $goem_col
132            = mb_substr(
133                $spatial,
134                19,
135                mb_strlen($spatial) - 20
136            );
137        // Split the geometry collection object to get its constituents.
138        $sub_parts = $this->explodeGeomCol($goem_col);
139
140        foreach ($sub_parts as $sub_part) {
141            $type_pos = mb_strpos($sub_part, '(');
142            if ($type_pos === false) {
143                continue;
144            }
145            $type = mb_substr($sub_part, 0, $type_pos);
146
147            $gis_obj = GisFactory::factory($type);
148            if (! $gis_obj) {
149                continue;
150            }
151            $image = $gis_obj->prepareRowAsPng(
152                $sub_part,
153                $label,
154                $color,
155                $scale_data,
156                $image
157            );
158        }
159
160        return $image;
161    }
162
163    /**
164     * Adds to the TCPDF instance, the data related to a row in the GIS dataset.
165     *
166     * @param string      $spatial    GIS GEOMETRYCOLLECTION object
167     * @param string|null $label      label for the GIS GEOMETRYCOLLECTION object
168     * @param string      $color      color for the GIS GEOMETRYCOLLECTION object
169     * @param array       $scale_data array containing data related to scaling
170     * @param TCPDF       $pdf        TCPDF instance
171     *
172     * @return TCPDF the modified TCPDF instance
173     *
174     * @access public
175     */
176    public function prepareRowAsPdf($spatial, ?string $label, $color, array $scale_data, $pdf)
177    {
178        // Trim to remove leading 'GEOMETRYCOLLECTION(' and trailing ')'
179        $goem_col
180            = mb_substr(
181                $spatial,
182                19,
183                mb_strlen($spatial) - 20
184            );
185        // Split the geometry collection object to get its constituents.
186        $sub_parts = $this->explodeGeomCol($goem_col);
187
188        foreach ($sub_parts as $sub_part) {
189            $type_pos = mb_strpos($sub_part, '(');
190            if ($type_pos === false) {
191                continue;
192            }
193            $type = mb_substr($sub_part, 0, $type_pos);
194
195            $gis_obj = GisFactory::factory($type);
196            if (! $gis_obj) {
197                continue;
198            }
199            $pdf = $gis_obj->prepareRowAsPdf(
200                $sub_part,
201                $label,
202                $color,
203                $scale_data,
204                $pdf
205            );
206        }
207
208        return $pdf;
209    }
210
211    /**
212     * Prepares and returns the code related to a row in the GIS dataset as SVG.
213     *
214     * @param string $spatial    GIS GEOMETRYCOLLECTION object
215     * @param string $label      label for the GIS GEOMETRYCOLLECTION object
216     * @param string $color      color for the GIS GEOMETRYCOLLECTION object
217     * @param array  $scale_data array containing data related to scaling
218     *
219     * @return string the code related to a row in the GIS dataset
220     *
221     * @access public
222     */
223    public function prepareRowAsSvg($spatial, $label, $color, array $scale_data)
224    {
225        $row = '';
226
227        // Trim to remove leading 'GEOMETRYCOLLECTION(' and trailing ')'
228        $goem_col
229            = mb_substr(
230                $spatial,
231                19,
232                mb_strlen($spatial) - 20
233            );
234        // Split the geometry collection object to get its constituents.
235        $sub_parts = $this->explodeGeomCol($goem_col);
236
237        foreach ($sub_parts as $sub_part) {
238            $type_pos = mb_strpos($sub_part, '(');
239            if ($type_pos === false) {
240                continue;
241            }
242            $type = mb_substr($sub_part, 0, $type_pos);
243
244            $gis_obj = GisFactory::factory($type);
245            if (! $gis_obj) {
246                continue;
247            }
248            $row .= $gis_obj->prepareRowAsSvg(
249                $sub_part,
250                $label,
251                $color,
252                $scale_data
253            );
254        }
255
256        return $row;
257    }
258
259    /**
260     * Prepares JavaScript related to a row in the GIS dataset
261     * to visualize it with OpenLayers.
262     *
263     * @param string $spatial    GIS GEOMETRYCOLLECTION object
264     * @param int    $srid       spatial reference ID
265     * @param string $label      label for the GIS GEOMETRYCOLLECTION object
266     * @param array  $color      color for the GIS GEOMETRYCOLLECTION object
267     * @param array  $scale_data array containing data related to scaling
268     *
269     * @return string JavaScript related to a row in the GIS dataset
270     *
271     * @access public
272     */
273    public function prepareRowAsOl($spatial, $srid, $label, $color, array $scale_data)
274    {
275        $row = '';
276
277        // Trim to remove leading 'GEOMETRYCOLLECTION(' and trailing ')'
278        $goem_col
279            = mb_substr(
280                $spatial,
281                19,
282                mb_strlen($spatial) - 20
283            );
284        // Split the geometry collection object to get its constituents.
285        $sub_parts = $this->explodeGeomCol($goem_col);
286
287        foreach ($sub_parts as $sub_part) {
288            $type_pos = mb_strpos($sub_part, '(');
289            if ($type_pos === false) {
290                continue;
291            }
292            $type = mb_substr($sub_part, 0, $type_pos);
293
294            $gis_obj = GisFactory::factory($type);
295            if (! $gis_obj) {
296                continue;
297            }
298            $row .= $gis_obj->prepareRowAsOl(
299                $sub_part,
300                $srid,
301                $label,
302                $color,
303                $scale_data
304            );
305        }
306
307        return $row;
308    }
309
310    /**
311     * Splits the GEOMETRYCOLLECTION object and get its constituents.
312     *
313     * @param string $geom_col geometry collection string
314     *
315     * @return array the constituents of the geometry collection object
316     *
317     * @access private
318     */
319    private function explodeGeomCol($geom_col)
320    {
321        $sub_parts = [];
322        $br_count = 0;
323        $start = 0;
324        $count = 0;
325        foreach (str_split($geom_col) as $char) {
326            if ($char === '(') {
327                $br_count++;
328            } elseif ($char === ')') {
329                $br_count--;
330                if ($br_count == 0) {
331                    $sub_parts[]
332                        = mb_substr(
333                            $geom_col,
334                            $start,
335                            $count + 1 - $start
336                        );
337                    $start = $count + 2;
338                }
339            }
340            $count++;
341        }
342
343        return $sub_parts;
344    }
345
346    /**
347     * Generates the WKT with the set of parameters passed by the GIS editor.
348     *
349     * @param array  $gis_data GIS data
350     * @param int    $index    index into the parameter object
351     * @param string $empty    value for empty points
352     *
353     * @return string WKT with the set of parameters passed by the GIS editor
354     *
355     * @access public
356     */
357    public function generateWkt(array $gis_data, $index, $empty = '')
358    {
359        $geom_count = $gis_data['GEOMETRYCOLLECTION']['geom_count'] ?? 1;
360        $wkt = 'GEOMETRYCOLLECTION(';
361        for ($i = 0; $i < $geom_count; $i++) {
362            if (! isset($gis_data[$i]['gis_type'])) {
363                continue;
364            }
365
366            $type = $gis_data[$i]['gis_type'];
367            $gis_obj = GisFactory::factory($type);
368            if (! $gis_obj) {
369                continue;
370            }
371            $wkt .= $gis_obj->generateWkt($gis_data, $i, $empty) . ',';
372        }
373        if (isset($gis_data[0]['gis_type'])) {
374            $wkt
375                = mb_substr(
376                    $wkt,
377                    0,
378                    mb_strlen($wkt) - 1
379                );
380        }
381
382        return $wkt . ')';
383    }
384
385    /**
386     * Generates parameters for the GIS data editor from the value of the GIS column.
387     *
388     * @param string $value of the GIS column
389     *
390     * @return array parameters for the GIS editor from the value of the GIS column
391     *
392     * @access public
393     */
394    public function generateParams($value)
395    {
396        $params = [];
397        $data = GisGeometry::generateParams($value);
398        $params['srid'] = $data['srid'];
399        $wkt = $data['wkt'];
400
401        // Trim to remove leading 'GEOMETRYCOLLECTION(' and trailing ')'
402        $goem_col
403            = mb_substr(
404                $wkt,
405                19,
406                mb_strlen($wkt) - 20
407            );
408        // Split the geometry collection object to get its constituents.
409        $sub_parts = $this->explodeGeomCol($goem_col);
410        $params['GEOMETRYCOLLECTION']['geom_count'] = count($sub_parts);
411
412        $i = 0;
413        foreach ($sub_parts as $sub_part) {
414            $type_pos = mb_strpos($sub_part, '(');
415            if ($type_pos === false) {
416                continue;
417            }
418            $type = mb_substr($sub_part, 0, $type_pos);
419            /**
420             * @var GisMultiPolygon|GisPolygon|GisMultiPoint|GisPoint|GisMultiLineString|GisLineString $gis_obj
421             */
422            $gis_obj = GisFactory::factory($type);
423            if (! $gis_obj) {
424                continue;
425            }
426            $params = array_merge($params, $gis_obj->generateParams($sub_part, $i));
427            $i++;
428        }
429
430        return $params;
431    }
432}
433