GisVisualization.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. <?php
  2. /**
  3. * Handles visualization of GIS data
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Gis;
  7. use PhpMyAdmin\Core;
  8. use PhpMyAdmin\Sanitize;
  9. use PhpMyAdmin\Util;
  10. use TCPDF;
  11. use const PNG_ALL_FILTERS;
  12. use function array_merge;
  13. use function base64_encode;
  14. use function count;
  15. use function imagecolorallocate;
  16. use function imagecreatetruecolor;
  17. use function imagedestroy;
  18. use function imagefilledrectangle;
  19. use function imagepng;
  20. use function intval;
  21. use function is_numeric;
  22. use function mb_strlen;
  23. use function mb_strpos;
  24. use function mb_strtolower;
  25. use function mb_substr;
  26. use function ob_get_clean;
  27. use function ob_start;
  28. use function is_string;
  29. /**
  30. * Handles visualization of GIS data
  31. */
  32. class GisVisualization
  33. {
  34. /** @var array Raw data for the visualization */
  35. private $data;
  36. /** @var string */
  37. private $modifiedSql;
  38. /** @var array Set of default settings values are here. */
  39. private $settings = [
  40. // Array of colors to be used for GIS visualizations.
  41. 'colors' => [
  42. '#B02EE0',
  43. '#E0642E',
  44. '#E0D62E',
  45. '#2E97E0',
  46. '#BCE02E',
  47. '#E02E75',
  48. '#5CE02E',
  49. '#E0B02E',
  50. '#0022E0',
  51. '#726CB1',
  52. '#481A36',
  53. '#BAC658',
  54. '#127224',
  55. '#825119',
  56. '#238C74',
  57. '#4C489B',
  58. '#87C9BF',
  59. ],
  60. // Hex values for abovementioned colours
  61. 'colors_hex' => [
  62. [176, 46, 224],
  63. [224, 100, 46],
  64. [224, 214, 46],
  65. [46, 151, 224],
  66. [188, 224, 46],
  67. [224, 46, 117],
  68. [92, 224, 46],
  69. [224, 176, 46],
  70. [0, 34, 224],
  71. [114, 108, 177],
  72. [72, 26, 54],
  73. [186, 198, 88],
  74. [18, 114, 36],
  75. [130, 81, 25],
  76. [35, 140, 116],
  77. [76, 72, 155],
  78. [135, 201, 191],
  79. ],
  80. // The width of the GIS visualization.
  81. 'width' => 600,
  82. // The height of the GIS visualization.
  83. 'height' => 450,
  84. ];
  85. /** @var array Options that the user has specified. */
  86. private $userSpecifiedSettings = null;
  87. /**
  88. * Returns the settings array
  89. *
  90. * @return array the settings array
  91. *
  92. * @access public
  93. */
  94. public function getSettings()
  95. {
  96. return $this->settings;
  97. }
  98. /**
  99. * Factory
  100. *
  101. * @param string $sql_query SQL to fetch raw data for visualization
  102. * @param array $options Users specified options
  103. * @param int $row number of rows
  104. * @param int $pos start position
  105. *
  106. * @return GisVisualization
  107. *
  108. * @access public
  109. */
  110. public static function get($sql_query, array $options, $row, $pos)
  111. {
  112. return new GisVisualization($sql_query, $options, $row, $pos);
  113. }
  114. /**
  115. * Get visualization
  116. *
  117. * @param array $data Raw data, if set, parameters other than $options will be
  118. * ignored
  119. * @param array $options Users specified options
  120. *
  121. * @return GisVisualization
  122. */
  123. public static function getByData(array $data, array $options)
  124. {
  125. return new GisVisualization(null, $options, null, null, $data);
  126. }
  127. /**
  128. * Check if data has SRID
  129. *
  130. * @return bool
  131. */
  132. public function hasSrid()
  133. {
  134. foreach ($this->data as $row) {
  135. if ($row['srid'] != 0) {
  136. return true;
  137. }
  138. }
  139. return false;
  140. }
  141. /**
  142. * Stores user specified options.
  143. *
  144. * @param string $sql_query SQL to fetch raw data for visualization
  145. * @param array $options Users specified options
  146. * @param int $row number of rows
  147. * @param int $pos start position
  148. * @param array|null $data raw data. If set, parameters other than $options
  149. * will be ignored
  150. *
  151. * @access public
  152. */
  153. private function __construct($sql_query, array $options, $row, $pos, $data = null)
  154. {
  155. $this->userSpecifiedSettings = $options;
  156. if (isset($data)) {
  157. $this->data = $data;
  158. } else {
  159. $this->modifiedSql = $this->modifySqlQuery($sql_query, $row, $pos);
  160. $this->data = $this->fetchRawData();
  161. }
  162. }
  163. /**
  164. * All the variable initialization, options handling has to be done here.
  165. *
  166. * @return void
  167. *
  168. * @access protected
  169. */
  170. protected function init()
  171. {
  172. $this->handleOptions();
  173. }
  174. /**
  175. * Returns sql for fetching raw data
  176. *
  177. * @param string $sql_query The SQL to modify.
  178. * @param int $rows Number of rows.
  179. * @param int $pos Start position.
  180. *
  181. * @return string the modified sql query.
  182. */
  183. private function modifySqlQuery($sql_query, $rows, $pos)
  184. {
  185. $isMariaDb = $this->userSpecifiedSettings['isMariaDB'] === true;
  186. $modified_query = 'SELECT ';
  187. $spatialAsText = 'ASTEXT';
  188. $spatialSrid = 'SRID';
  189. $axisOrder = '';
  190. if ($this->userSpecifiedSettings['mysqlVersion'] >= 50600) {
  191. $spatialAsText = 'ST_ASTEXT';
  192. $spatialSrid = 'ST_SRID';
  193. }
  194. // If MYSQL version >= 8.1 override default axis order
  195. if ($this->userSpecifiedSettings['mysqlVersion'] >= 80010 && ! $isMariaDb) {
  196. $axisOrder = ', \'axis-order=long-lat\'';
  197. }
  198. // If label column is chosen add it to the query
  199. if (! empty($this->userSpecifiedSettings['labelColumn'])) {
  200. $modified_query .= Util::backquote(
  201. $this->userSpecifiedSettings['labelColumn']
  202. )
  203. . ', ';
  204. }
  205. // Wrap the spatial column with 'ST_ASTEXT()' function and add it
  206. $modified_query .= $spatialAsText . '('
  207. . Util::backquote($this->userSpecifiedSettings['spatialColumn'])
  208. . $axisOrder . ') AS ' . Util::backquote(
  209. $this->userSpecifiedSettings['spatialColumn']
  210. )
  211. . ', ';
  212. // Get the SRID
  213. $modified_query .= $spatialSrid . '('
  214. . Util::backquote($this->userSpecifiedSettings['spatialColumn'])
  215. . ') AS ' . Util::backquote('srid') . ' ';
  216. // Append the original query as the inner query
  217. $modified_query .= 'FROM (' . $sql_query . ') AS '
  218. . Util::backquote('temp_gis');
  219. // LIMIT clause
  220. if (is_numeric($rows) && $rows > 0) {
  221. $modified_query .= ' LIMIT ';
  222. if (is_numeric($pos) && $pos >= 0) {
  223. $modified_query .= $pos . ', ' . $rows;
  224. } else {
  225. $modified_query .= $rows;
  226. }
  227. }
  228. return $modified_query;
  229. }
  230. /**
  231. * Returns raw data for GIS visualization.
  232. *
  233. * @return array the raw data.
  234. */
  235. private function fetchRawData()
  236. {
  237. global $dbi;
  238. $modified_result = $dbi->tryQuery($this->modifiedSql);
  239. if ($modified_result === false) {
  240. return [];
  241. }
  242. $data = [];
  243. while ($row = $dbi->fetchAssoc($modified_result)) {
  244. $data[] = $row;
  245. }
  246. return $data;
  247. }
  248. /**
  249. * A function which handles passed parameters. Useful if desired
  250. * chart needs to be a little bit different from the default one.
  251. *
  252. * @return void
  253. *
  254. * @access private
  255. */
  256. private function handleOptions()
  257. {
  258. if ($this->userSpecifiedSettings === null) {
  259. return;
  260. }
  261. $this->settings = array_merge(
  262. $this->settings,
  263. $this->userSpecifiedSettings
  264. );
  265. }
  266. /**
  267. * Sanitizes the file name.
  268. *
  269. * @param string $file_name file name
  270. * @param string $ext extension of the file
  271. *
  272. * @return string the sanitized file name
  273. *
  274. * @access private
  275. */
  276. private function sanitizeName($file_name, $ext)
  277. {
  278. $file_name = Sanitize::sanitizeFilename($file_name);
  279. // Check if the user already added extension;
  280. // get the substring where the extension would be if it was included
  281. $extension_start_pos = mb_strlen($file_name) - mb_strlen($ext) - 1;
  282. $user_extension
  283. = mb_substr(
  284. $file_name,
  285. $extension_start_pos,
  286. mb_strlen($file_name)
  287. );
  288. $required_extension = '.' . $ext;
  289. if (mb_strtolower($user_extension) != $required_extension) {
  290. $file_name .= $required_extension;
  291. }
  292. return $file_name;
  293. }
  294. /**
  295. * Handles common tasks of writing the visualization to file for various formats.
  296. *
  297. * @param string $file_name file name
  298. * @param string $type mime type
  299. * @param string $ext extension of the file
  300. *
  301. * @return void
  302. *
  303. * @access private
  304. */
  305. private function writeToFile($file_name, $type, $ext)
  306. {
  307. $file_name = $this->sanitizeName($file_name, $ext);
  308. Core::downloadHeader($file_name, $type);
  309. }
  310. /**
  311. * Generate the visualization in SVG format.
  312. *
  313. * @return string the generated image resource
  314. *
  315. * @access private
  316. */
  317. private function svg()
  318. {
  319. $this->init();
  320. $output = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'
  321. . "\n"
  322. . '<svg version="1.1" xmlns:svg="http://www.w3.org/2000/svg"'
  323. . ' xmlns="http://www.w3.org/2000/svg"'
  324. . ' width="' . intval($this->settings['width']) . '"'
  325. . ' height="' . intval($this->settings['height']) . '">'
  326. . '<g id="groupPanel">';
  327. $scale_data = $this->scaleDataSet($this->data);
  328. $output .= $this->prepareDataSet($this->data, $scale_data, 'svg', '');
  329. $output .= '</g></svg>';
  330. return $output;
  331. }
  332. /**
  333. * Get the visualization as a SVG.
  334. *
  335. * @return string the visualization as a SVG
  336. *
  337. * @access public
  338. */
  339. public function asSVG()
  340. {
  341. return $this->svg();
  342. }
  343. /**
  344. * Saves as a SVG image to a file.
  345. *
  346. * @param string $file_name File name
  347. *
  348. * @return void
  349. *
  350. * @access public
  351. */
  352. public function toFileAsSvg($file_name)
  353. {
  354. $img = $this->svg();
  355. $this->writeToFile($file_name, 'image/svg+xml', 'svg');
  356. echo $img;
  357. }
  358. /**
  359. * Generate the visualization in PNG format.
  360. *
  361. * @return resource the generated image resource
  362. *
  363. * @access private
  364. */
  365. private function png()
  366. {
  367. $this->init();
  368. // create image
  369. $image = imagecreatetruecolor(
  370. $this->settings['width'],
  371. $this->settings['height']
  372. );
  373. // fill the background
  374. $bg = imagecolorallocate($image, 229, 229, 229);
  375. imagefilledrectangle(
  376. $image,
  377. 0,
  378. 0,
  379. $this->settings['width'] - 1,
  380. $this->settings['height'] - 1,
  381. $bg
  382. );
  383. $scale_data = $this->scaleDataSet($this->data);
  384. $image = $this->prepareDataSet($this->data, $scale_data, 'png', $image);
  385. return $image;
  386. }
  387. /**
  388. * Get the visualization as a PNG.
  389. *
  390. * @return string the visualization as a PNG
  391. *
  392. * @access public
  393. */
  394. public function asPng()
  395. {
  396. $img = $this->png();
  397. // render and save it to variable
  398. ob_start();
  399. imagepng($img, null, 9, PNG_ALL_FILTERS);
  400. imagedestroy($img);
  401. $output = ob_get_clean();
  402. // base64 encode
  403. $encoded = base64_encode((string) $output);
  404. return '<img src="data:image/png;base64,' . $encoded . '">';
  405. }
  406. /**
  407. * Saves as a PNG image to a file.
  408. *
  409. * @param string $file_name File name
  410. *
  411. * @return void
  412. *
  413. * @access public
  414. */
  415. public function toFileAsPng($file_name)
  416. {
  417. $img = $this->png();
  418. $this->writeToFile($file_name, 'image/png', 'png');
  419. imagepng($img, null, 9, PNG_ALL_FILTERS);
  420. imagedestroy($img);
  421. }
  422. /**
  423. * Get the code for visualization with OpenLayers.
  424. *
  425. * @return string the code for visualization with OpenLayers
  426. *
  427. * @todo Should return JSON to avoid eval() in gis_data_editor.js
  428. * @access public
  429. */
  430. public function asOl()
  431. {
  432. $this->init();
  433. $scale_data = $this->scaleDataSet($this->data);
  434. $output = 'if (typeof ol !== "undefined") {'
  435. . 'var olCss = "js/vendor/openlayers/theme/ol.css";'
  436. . '$(\'head\').append(\'<link rel="stylesheet" type="text/css" href=\'+olCss+\'>\');'
  437. . 'var vectorLayer = new ol.source.Vector({});'
  438. . 'var map = new ol.Map({'
  439. . 'target: \'openlayersmap\','
  440. . 'layers: ['
  441. . 'new ol.layer.Tile({'
  442. . 'source: new ol.source.OSM()'
  443. . '}),'
  444. . 'new ol.layer.Vector({'
  445. . 'source: vectorLayer'
  446. . '})'
  447. . '],'
  448. . 'view: new ol.View({'
  449. . 'center: ol.proj.fromLonLat([37.41, 8.82]),'
  450. . 'zoom: 4'
  451. . '}),'
  452. . 'controls: [new ol.control.MousePosition({'
  453. . 'coordinateFormat: ol.coordinate.createStringXY(4),'
  454. . 'projection: \'EPSG:4326\'}),'
  455. . 'new ol.control.Zoom,'
  456. . 'new ol.control.Attribution]'
  457. . '});';
  458. $output .= $this->prepareDataSet($this->data, $scale_data, 'ol', '')
  459. . '}';
  460. return $output;
  461. }
  462. /**
  463. * Saves as a PDF to a file.
  464. *
  465. * @param string $file_name File name
  466. *
  467. * @return void
  468. *
  469. * @access public
  470. */
  471. public function toFileAsPdf($file_name)
  472. {
  473. $this->init();
  474. // create pdf
  475. $pdf = new TCPDF(
  476. '',
  477. 'pt',
  478. $GLOBALS['cfg']['PDFDefaultPageSize'],
  479. true,
  480. 'UTF-8',
  481. false
  482. );
  483. // disable header and footer
  484. $pdf->setPrintHeader(false);
  485. $pdf->setPrintFooter(false);
  486. //set auto page breaks
  487. $pdf->SetAutoPageBreak(false);
  488. // add a page
  489. $pdf->AddPage();
  490. $scale_data = $this->scaleDataSet($this->data);
  491. $pdf = $this->prepareDataSet($this->data, $scale_data, 'pdf', $pdf);
  492. // sanitize file name
  493. $file_name = $this->sanitizeName($file_name, 'pdf');
  494. $pdf->Output($file_name, 'D');
  495. }
  496. /**
  497. * Convert file to image
  498. *
  499. * @param string $format Output format
  500. *
  501. * @return string File
  502. */
  503. public function toImage($format)
  504. {
  505. if ($format === 'svg') {
  506. return $this->asSVG();
  507. }
  508. if ($format === 'png') {
  509. return $this->asPng();
  510. }
  511. if ($format === 'ol') {
  512. return $this->asOl();
  513. }
  514. return '';
  515. }
  516. /**
  517. * Convert file to given format
  518. *
  519. * @param string $filename Filename
  520. * @param string $format Output format
  521. *
  522. * @return void
  523. */
  524. public function toFile($filename, $format)
  525. {
  526. if ($format === 'svg') {
  527. $this->toFileAsSvg($filename);
  528. } elseif ($format === 'png') {
  529. $this->toFileAsPng($filename);
  530. } elseif ($format === 'pdf') {
  531. $this->toFileAsPdf($filename);
  532. }
  533. }
  534. /**
  535. * Calculates the scale, horizontal and vertical offset that should be used.
  536. *
  537. * @param array $data Row data
  538. *
  539. * @return array an array containing the scale, x and y offsets
  540. *
  541. * @access private
  542. */
  543. private function scaleDataSet(array $data)
  544. {
  545. $min_max = [
  546. 'maxX' => 0.0,
  547. 'maxY' => 0.0,
  548. 'minX' => 0.0,
  549. 'minY' => 0.0,
  550. ];
  551. $border = 15;
  552. // effective width and height of the plot
  553. $plot_width = $this->settings['width'] - 2 * $border;
  554. $plot_height = $this->settings['height'] - 2 * $border;
  555. foreach ($data as $row) {
  556. // Figure out the data type
  557. $ref_data = $row[$this->settings['spatialColumn']];
  558. if (! is_string($ref_data)) {
  559. continue;
  560. }
  561. $type_pos = mb_strpos($ref_data, '(');
  562. if ($type_pos === false) {
  563. continue;
  564. }
  565. $type = mb_substr($ref_data, 0, $type_pos);
  566. $gis_obj = GisFactory::factory($type);
  567. if (! $gis_obj) {
  568. continue;
  569. }
  570. $scale_data = $gis_obj->scaleRow(
  571. $row[$this->settings['spatialColumn']]
  572. );
  573. // Update minimum/maximum values for x and y coordinates.
  574. $c_maxX = (float) $scale_data['maxX'];
  575. if ($min_max['maxX'] === 0.0 || $c_maxX > $min_max['maxX']) {
  576. $min_max['maxX'] = $c_maxX;
  577. }
  578. $c_minX = (float) $scale_data['minX'];
  579. if ($min_max['minX'] === 0.0 || $c_minX < $min_max['minX']) {
  580. $min_max['minX'] = $c_minX;
  581. }
  582. $c_maxY = (float) $scale_data['maxY'];
  583. if ($min_max['maxY'] === 0.0 || $c_maxY > $min_max['maxY']) {
  584. $min_max['maxY'] = $c_maxY;
  585. }
  586. $c_minY = (float) $scale_data['minY'];
  587. if ($min_max['minY'] !== 0.0 && $c_minY >= $min_max['minY']) {
  588. continue;
  589. }
  590. $min_max['minY'] = $c_minY;
  591. }
  592. // scale the visualization
  593. $x_ratio = ($min_max['maxX'] - $min_max['minX']) / $plot_width;
  594. $y_ratio = ($min_max['maxY'] - $min_max['minY']) / $plot_height;
  595. $ratio = $x_ratio > $y_ratio ? $x_ratio : $y_ratio;
  596. $scale = $ratio != 0 ? 1 / $ratio : 1;
  597. if ($x_ratio < $y_ratio) {
  598. // center horizontally
  599. $x = ($min_max['maxX'] + $min_max['minX'] - $plot_width / $scale) / 2;
  600. // fit vertically
  601. $y = $min_max['minY'] - ($border / $scale);
  602. } else {
  603. // fit horizontally
  604. $x = $min_max['minX'] - ($border / $scale);
  605. // center vertically
  606. $y = ($min_max['maxY'] + $min_max['minY'] - $plot_height / $scale) / 2;
  607. }
  608. return [
  609. 'scale' => $scale,
  610. 'x' => $x,
  611. 'y' => $y,
  612. 'minX' => $min_max['minX'],
  613. 'maxX' => $min_max['maxX'],
  614. 'minY' => $min_max['minY'],
  615. 'maxY' => $min_max['maxY'],
  616. 'height' => $this->settings['height'],
  617. ];
  618. }
  619. /**
  620. * Prepares and return the dataset as needed by the visualization.
  621. *
  622. * @param array $data Raw data
  623. * @param array $scale_data Data related to scaling
  624. * @param string $format Format of the visualization
  625. * @param resource|TCPDF|string|false $results Image object in the case of png
  626. * TCPDF object in the case of pdf
  627. *
  628. * @return mixed the formatted array of data
  629. *
  630. * @access private
  631. */
  632. private function prepareDataSet(array $data, array $scale_data, $format, $results)
  633. {
  634. $color_number = 0;
  635. // loop through the rows
  636. foreach ($data as $row) {
  637. $index = $color_number % count($this->settings['colors']);
  638. // Figure out the data type
  639. $ref_data = $row[$this->settings['spatialColumn']];
  640. if (! is_string($ref_data)) {
  641. continue;
  642. }
  643. $type_pos = mb_strpos($ref_data, '(');
  644. if ($type_pos === false) {
  645. continue;
  646. }
  647. $type = mb_substr($ref_data, 0, $type_pos);
  648. $gis_obj = GisFactory::factory($type);
  649. if (! $gis_obj) {
  650. continue;
  651. }
  652. $label = '';
  653. if (isset($this->settings['labelColumn'], $row[$this->settings['labelColumn']])) {
  654. $label = $row[$this->settings['labelColumn']];
  655. }
  656. if ($format === 'svg') {
  657. $results .= $gis_obj->prepareRowAsSvg(
  658. $row[$this->settings['spatialColumn']],
  659. $label,
  660. $this->settings['colors'][$index],
  661. $scale_data
  662. );
  663. } elseif ($format === 'png') {
  664. $results = $gis_obj->prepareRowAsPng(
  665. $row[$this->settings['spatialColumn']],
  666. $label,
  667. $this->settings['colors'][$index],
  668. $scale_data,
  669. $results
  670. );
  671. } elseif ($format === 'pdf' && $results instanceof TCPDF) {
  672. $results = $gis_obj->prepareRowAsPdf(
  673. $row[$this->settings['spatialColumn']],
  674. $label,
  675. $this->settings['colors'][$index],
  676. $scale_data,
  677. $results
  678. );
  679. } elseif ($format === 'ol') {
  680. $results .= $gis_obj->prepareRowAsOl(
  681. $row[$this->settings['spatialColumn']],
  682. $row['srid'],
  683. $label,
  684. $this->settings['colors_hex'][$index],
  685. $scale_data
  686. );
  687. }
  688. $color_number++;
  689. }
  690. return $results;
  691. }
  692. /**
  693. * Set user specified settings
  694. *
  695. * @param array $userSpecifiedSettings User specified settings
  696. *
  697. * @return void
  698. */
  699. public function setUserSpecifiedSettings(array $userSpecifiedSettings)
  700. {
  701. $this->userSpecifiedSettings = $userSpecifiedSettings;
  702. }
  703. }