GisVisualization.php 21 KB

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