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