1<?php
2/**
3 * Handles actions related to GIS MULTILINESTRING objects
4 */
5
6declare(strict_types=1);
7
8namespace PhpMyAdmin\Gis;
9
10use TCPDF;
11use function count;
12use function explode;
13use function hexdec;
14use function imagecolorallocate;
15use function imageline;
16use function imagestring;
17use function json_encode;
18use function mb_strlen;
19use function mb_substr;
20use function round;
21use function trim;
22
23/**
24 * Handles actions related to GIS MULTILINESTRING objects
25 */
26class GisMultiLineString extends GisGeometry
27{
28    /** @var self */
29    private static $instance;
30
31    /**
32     * A private constructor; prevents direct creation of object.
33     *
34     * @access private
35     */
36    private function __construct()
37    {
38    }
39
40    /**
41     * Returns the singleton.
42     *
43     * @return GisMultiLineString the singleton
44     *
45     * @access public
46     */
47    public static function singleton()
48    {
49        if (! isset(self::$instance)) {
50            self::$instance = new GisMultiLineString();
51        }
52
53        return self::$instance;
54    }
55
56    /**
57     * Scales each row.
58     *
59     * @param string $spatial spatial data of a row
60     *
61     * @return array an array containing the min, max values for x and y coordinates
62     *
63     * @access public
64     */
65    public function scaleRow($spatial)
66    {
67        $min_max = [];
68
69        // Trim to remove leading 'MULTILINESTRING((' and trailing '))'
70        $multilinestirng
71            = mb_substr(
72                $spatial,
73                17,
74                mb_strlen($spatial) - 19
75            );
76        // Separate each linestring
77        $linestirngs = explode('),(', $multilinestirng);
78
79        foreach ($linestirngs as $linestring) {
80            $min_max = $this->setMinMax($linestring, $min_max);
81        }
82
83        return $min_max;
84    }
85
86    /**
87     * Adds to the PNG image object, the data related to a row in the GIS dataset.
88     *
89     * @param string      $spatial    GIS POLYGON object
90     * @param string|null $label      Label for the GIS POLYGON object
91     * @param string      $line_color Color for the GIS POLYGON object
92     * @param array       $scale_data Array containing data related to scaling
93     * @param resource    $image      Image object
94     *
95     * @return resource the modified image object
96     *
97     * @access public
98     */
99    public function prepareRowAsPng(
100        $spatial,
101        ?string $label,
102        $line_color,
103        array $scale_data,
104        $image
105    ) {
106        // allocate colors
107        $black = imagecolorallocate($image, 0, 0, 0);
108        $red = hexdec(mb_substr($line_color, 1, 2));
109        $green = hexdec(mb_substr($line_color, 3, 2));
110        $blue = hexdec(mb_substr($line_color, 4, 2));
111        $color = imagecolorallocate($image, $red, $green, $blue);
112
113        // Trim to remove leading 'MULTILINESTRING((' and trailing '))'
114        $multilinestirng
115            = mb_substr(
116                $spatial,
117                17,
118                mb_strlen($spatial) - 19
119            );
120        // Separate each linestring
121        $linestirngs = explode('),(', $multilinestirng);
122
123        $first_line = true;
124        foreach ($linestirngs as $linestring) {
125            $points_arr = $this->extractPoints($linestring, $scale_data);
126            foreach ($points_arr as $point) {
127                if (isset($temp_point)) {
128                    // draw line section
129                    imageline(
130                        $image,
131                        (int) round($temp_point[0]),
132                        (int) round($temp_point[1]),
133                        (int) round($point[0]),
134                        (int) round($point[1]),
135                        $color
136                    );
137                }
138                $temp_point = $point;
139            }
140            unset($temp_point);
141            // print label if applicable
142            if (isset($label) && trim($label) != '' && $first_line) {
143                imagestring(
144                    $image,
145                    1,
146                    (int) round($points_arr[1][0]),
147                    (int) round($points_arr[1][1]),
148                    trim($label),
149                    $black
150                );
151            }
152            $first_line = false;
153        }
154
155        return $image;
156    }
157
158    /**
159     * Adds to the TCPDF instance, the data related to a row in the GIS dataset.
160     *
161     * @param string      $spatial    GIS MULTILINESTRING object
162     * @param string|null $label      Label for the GIS MULTILINESTRING object
163     * @param string      $line_color Color for the GIS MULTILINESTRING object
164     * @param array       $scale_data Array containing data related to scaling
165     * @param TCPDF       $pdf        TCPDF instance
166     *
167     * @return TCPDF the modified TCPDF instance
168     *
169     * @access public
170     */
171    public function prepareRowAsPdf($spatial, ?string $label, $line_color, array $scale_data, $pdf)
172    {
173        // allocate colors
174        $red = hexdec(mb_substr($line_color, 1, 2));
175        $green = hexdec(mb_substr($line_color, 3, 2));
176        $blue = hexdec(mb_substr($line_color, 4, 2));
177        $line = [
178            'width' => 1.5,
179            'color' => [
180                $red,
181                $green,
182                $blue,
183            ],
184        ];
185
186        // Trim to remove leading 'MULTILINESTRING((' and trailing '))'
187        $multilinestirng
188            = mb_substr(
189                $spatial,
190                17,
191                mb_strlen($spatial) - 19
192            );
193        // Separate each linestring
194        $linestirngs = explode('),(', $multilinestirng);
195
196        $first_line = true;
197        foreach ($linestirngs as $linestring) {
198            $points_arr = $this->extractPoints($linestring, $scale_data);
199            foreach ($points_arr as $point) {
200                if (isset($temp_point)) {
201                    // draw line section
202                    $pdf->Line(
203                        $temp_point[0],
204                        $temp_point[1],
205                        $point[0],
206                        $point[1],
207                        $line
208                    );
209                }
210                $temp_point = $point;
211            }
212            unset($temp_point);
213            // print label
214            if (isset($label) && trim($label) != '' && $first_line) {
215                $pdf->SetXY($points_arr[1][0], $points_arr[1][1]);
216                $pdf->SetFontSize(5);
217                $pdf->Cell(0, 0, trim($label));
218            }
219            $first_line = false;
220        }
221
222        return $pdf;
223    }
224
225    /**
226     * Prepares and returns the code related to a row in the GIS dataset as SVG.
227     *
228     * @param string $spatial    GIS MULTILINESTRING object
229     * @param string $label      Label for the GIS MULTILINESTRING object
230     * @param string $line_color Color for the GIS MULTILINESTRING object
231     * @param array  $scale_data Array containing data related to scaling
232     *
233     * @return string the code related to a row in the GIS dataset
234     *
235     * @access public
236     */
237    public function prepareRowAsSvg($spatial, $label, $line_color, array $scale_data)
238    {
239        $line_options = [
240            'name'         => $label,
241            'class'        => 'linestring vector',
242            'fill'         => 'none',
243            'stroke'       => $line_color,
244            'stroke-width' => 2,
245        ];
246
247        // Trim to remove leading 'MULTILINESTRING((' and trailing '))'
248        $multilinestirng
249            = mb_substr(
250                $spatial,
251                17,
252                mb_strlen($spatial) - 19
253            );
254        // Separate each linestring
255        $linestirngs = explode('),(', $multilinestirng);
256
257        $row = '';
258        foreach ($linestirngs as $linestring) {
259            $points_arr = $this->extractPoints($linestring, $scale_data);
260
261            $row .= '<polyline points="';
262            foreach ($points_arr as $point) {
263                $row .= $point[0] . ',' . $point[1] . ' ';
264            }
265            $row .= '"';
266            $line_options['id'] = $label . $this->getRandomId();
267            foreach ($line_options as $option => $val) {
268                $row .= ' ' . $option . '="' . trim((string) $val) . '"';
269            }
270            $row .= '/>';
271        }
272
273        return $row;
274    }
275
276    /**
277     * Prepares JavaScript related to a row in the GIS dataset
278     * to visualize it with OpenLayers.
279     *
280     * @param string $spatial    GIS MULTILINESTRING object
281     * @param int    $srid       Spatial reference ID
282     * @param string $label      Label for the GIS MULTILINESTRING object
283     * @param array  $line_color Color for the GIS MULTILINESTRING object
284     * @param array  $scale_data Array containing data related to scaling
285     *
286     * @return string JavaScript related to a row in the GIS dataset
287     *
288     * @access public
289     */
290    public function prepareRowAsOl($spatial, $srid, $label, $line_color, array $scale_data)
291    {
292        $stroke_style = [
293            'color' => $line_color,
294            'width' => 2,
295        ];
296
297        $row =  'var style = new ol.style.Style({'
298            . 'stroke: new ol.style.Stroke(' . json_encode($stroke_style) . ')';
299        if (trim($label) !== '') {
300            $text_style = ['text' => trim($label)];
301            $row .= ', text: new ol.style.Text(' . json_encode($text_style) . ')';
302        }
303
304        $row .= '});';
305
306        if ($srid == 0) {
307            $srid = 4326;
308        }
309        $row .= $this->getBoundsForOl($srid, $scale_data);
310
311        // Trim to remove leading 'MULTILINESTRING((' and trailing '))'
312        $multilinestirng
313            = mb_substr(
314                $spatial,
315                17,
316                mb_strlen($spatial) - 19
317            );
318        // Separate each linestring
319        $linestirngs = explode('),(', $multilinestirng);
320
321        return $row . $this->getLineArrayForOpenLayers($linestirngs, $srid)
322            . 'var multiLineString = new ol.geom.MultiLineString(arr);'
323            . 'var feature = new ol.Feature({geometry: multiLineString});'
324            . 'feature.setStyle(style);'
325            . 'vectorLayer.addFeature(feature);';
326    }
327
328    /**
329     * Generate the WKT with the set of parameters passed by the GIS editor.
330     *
331     * @param array  $gis_data GIS data
332     * @param int    $index    Index into the parameter object
333     * @param string $empty    Value for empty points
334     *
335     * @return string WKT with the set of parameters passed by the GIS editor
336     *
337     * @access public
338     */
339    public function generateWkt(array $gis_data, $index, $empty = '')
340    {
341        $data_row = $gis_data[$index]['MULTILINESTRING'];
342
343        $no_of_lines = $data_row['no_of_lines'] ?? 1;
344        if ($no_of_lines < 1) {
345            $no_of_lines = 1;
346        }
347
348        $wkt = 'MULTILINESTRING(';
349        for ($i = 0; $i < $no_of_lines; $i++) {
350            $no_of_points = $data_row[$i]['no_of_points'] ?? 2;
351            if ($no_of_points < 2) {
352                $no_of_points = 2;
353            }
354            $wkt .= '(';
355            for ($j = 0; $j < $no_of_points; $j++) {
356                $wkt .= (isset($data_row[$i][$j]['x'])
357                        && trim((string) $data_row[$i][$j]['x']) != ''
358                        ? $data_row[$i][$j]['x'] : $empty)
359                    . ' ' . (isset($data_row[$i][$j]['y'])
360                        && trim((string) $data_row[$i][$j]['y']) != ''
361                        ? $data_row[$i][$j]['y'] : $empty) . ',';
362            }
363            $wkt
364                = mb_substr(
365                    $wkt,
366                    0,
367                    mb_strlen($wkt) - 1
368                );
369            $wkt .= '),';
370        }
371        $wkt
372            = mb_substr(
373                $wkt,
374                0,
375                mb_strlen($wkt) - 1
376            );
377
378        return $wkt . ')';
379    }
380
381    /**
382     * Generate the WKT for the data from ESRI shape files.
383     *
384     * @param array $row_data GIS data
385     *
386     * @return string the WKT for the data from ESRI shape files
387     *
388     * @access public
389     */
390    public function getShape(array $row_data)
391    {
392        $wkt = 'MULTILINESTRING(';
393        for ($i = 0; $i < $row_data['numparts']; $i++) {
394            $wkt .= '(';
395            foreach ($row_data['parts'][$i]['points'] as $point) {
396                $wkt .= $point['x'] . ' ' . $point['y'] . ',';
397            }
398            $wkt
399                = mb_substr(
400                    $wkt,
401                    0,
402                    mb_strlen($wkt) - 1
403                );
404            $wkt .= '),';
405        }
406        $wkt
407            = mb_substr(
408                $wkt,
409                0,
410                mb_strlen($wkt) - 1
411            );
412
413        return $wkt . ')';
414    }
415
416    /**
417     * Generate parameters for the GIS data editor from the value of the GIS column.
418     *
419     * @param string $value Value of the GIS column
420     * @param int    $index Index of the geometry
421     *
422     * @return array params for the GIS data editor from the value of the GIS column
423     *
424     * @access public
425     */
426    public function generateParams($value, $index = -1)
427    {
428        $params = [];
429        if ($index == -1) {
430            $index = 0;
431            $data = GisGeometry::generateParams($value);
432            $params['srid'] = $data['srid'];
433            $wkt = $data['wkt'];
434        } else {
435            $params[$index]['gis_type'] = 'MULTILINESTRING';
436            $wkt = $value;
437        }
438
439        // Trim to remove leading 'MULTILINESTRING((' and trailing '))'
440        $multilinestirng
441            = mb_substr(
442                $wkt,
443                17,
444                mb_strlen($wkt) - 19
445            );
446        // Separate each linestring
447        $linestirngs = explode('),(', $multilinestirng);
448        $params[$index]['MULTILINESTRING']['no_of_lines'] = count($linestirngs);
449
450        $j = 0;
451        foreach ($linestirngs as $linestring) {
452            $points_arr = $this->extractPoints($linestring, null);
453            $no_of_points = count($points_arr);
454            $params[$index]['MULTILINESTRING'][$j]['no_of_points'] = $no_of_points;
455            for ($i = 0; $i < $no_of_points; $i++) {
456                $params[$index]['MULTILINESTRING'][$j][$i]['x'] = $points_arr[$i][0];
457                $params[$index]['MULTILINESTRING'][$j][$i]['y'] = $points_arr[$i][1];
458            }
459            $j++;
460        }
461
462        return $params;
463    }
464}
465