GisVisualization.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  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. namespace PhpMyAdmin\Gis;
  9. use PhpMyAdmin\Core;
  10. use PhpMyAdmin\Sanitize;
  11. use PhpMyAdmin\Util;
  12. use TCPDF;
  13. /**
  14. * Handles visualization of GIS data
  15. *
  16. * @package PhpMyAdmin-GIS
  17. */
  18. class GisVisualization
  19. {
  20. /**
  21. * @var array Raw data for the visualization
  22. */
  23. private $_data;
  24. private $_modified_sql;
  25. /**
  26. * @var array Set of default settings values are here.
  27. */
  28. private $_settings = array(
  29. // Array of colors to be used for GIS visualizations.
  30. 'colors' => array(
  31. '#B02EE0',
  32. '#E0642E',
  33. '#E0D62E',
  34. '#2E97E0',
  35. '#BCE02E',
  36. '#E02E75',
  37. '#5CE02E',
  38. '#E0B02E',
  39. '#0022E0',
  40. '#726CB1',
  41. '#481A36',
  42. '#BAC658',
  43. '#127224',
  44. '#825119',
  45. '#238C74',
  46. '#4C489B',
  47. '#87C9BF',
  48. ),
  49. // The width of the GIS visualization.
  50. 'width' => 600,
  51. // The height of the GIS visualization.
  52. 'height' => 450,
  53. );
  54. /**
  55. * @var array Options that the user has specified.
  56. */
  57. private $_userSpecifiedSettings = null;
  58. /**
  59. * Returns the settings array
  60. *
  61. * @return array the settings array
  62. * @access public
  63. */
  64. public function getSettings()
  65. {
  66. return $this->_settings;
  67. }
  68. /**
  69. * Factory
  70. *
  71. * @param string $sql_query SQL to fetch raw data for visualization
  72. * @param array $options Users specified options
  73. * @param integer $row number of rows
  74. * @param integer $pos start position
  75. *
  76. * @return GisVisualization
  77. *
  78. * @access public
  79. */
  80. public static function get($sql_query, array $options, $row, $pos)
  81. {
  82. return new GisVisualization($sql_query, $options, $row, $pos);
  83. }
  84. /**
  85. * Get visualization
  86. *
  87. * @param array $data Raw data, if set, parameters other than $options will be
  88. * ignored
  89. * @param array $options Users specified options
  90. *
  91. * @return GisVisualization
  92. */
  93. public static function getByData(array $data, array $options)
  94. {
  95. return new GisVisualization(null, $options, null, null, $data);
  96. }
  97. /**
  98. * Check if data has SRID
  99. *
  100. * @return bool
  101. */
  102. public function hasSrid()
  103. {
  104. foreach ($this->_data as $row) {
  105. if ($row['srid'] != 0) {
  106. return true;
  107. }
  108. }
  109. return false;
  110. }
  111. /**
  112. * Constructor. Stores user specified options.
  113. *
  114. * @param string $sql_query SQL to fetch raw data for visualization
  115. * @param array $options Users specified options
  116. * @param integer $row number of rows
  117. * @param integer $pos start position
  118. * @param array|null $data raw data. If set, parameters other than $options
  119. * will be ignored
  120. *
  121. * @access public
  122. */
  123. private function __construct($sql_query, array $options, $row, $pos, $data = null)
  124. {
  125. $this->_userSpecifiedSettings = $options;
  126. if (isset($data)) {
  127. $this->_data = $data;
  128. } else {
  129. $this->_modified_sql = $this->_modifySqlQuery($sql_query, $row, $pos);
  130. $this->_data = $this->_fetchRawData();
  131. }
  132. }
  133. /**
  134. * All the variable initialization, options handling has to be done here.
  135. *
  136. * @return void
  137. * @access protected
  138. */
  139. protected function init()
  140. {
  141. $this->_handleOptions();
  142. }
  143. /**
  144. * Returns sql for fetching raw data
  145. *
  146. * @param string $sql_query The SQL to modify.
  147. * @param integer $rows Number of rows.
  148. * @param integer $pos Start position.
  149. *
  150. * @return string the modified sql query.
  151. */
  152. private function _modifySqlQuery($sql_query, $rows, $pos)
  153. {
  154. $modified_query = 'SELECT ';
  155. $spatialAsText = 'ASTEXT';
  156. $spatialSrid = 'SRID';
  157. if ($this->_userSpecifiedSettings['mysqlVersion'] >= 50600) {
  158. $spatialAsText = 'ST_ASTEXT';
  159. $spatialSrid = 'ST_SRID';
  160. }
  161. // If label column is chosen add it to the query
  162. if (!empty($this->_userSpecifiedSettings['labelColumn'])) {
  163. $modified_query .= Util::backquote(
  164. $this->_userSpecifiedSettings['labelColumn']
  165. )
  166. . ', ';
  167. }
  168. // Wrap the spatial column with 'ST_ASTEXT()' function and add it
  169. $modified_query .= $spatialAsText . '('
  170. . Util::backquote($this->_userSpecifiedSettings['spatialColumn'])
  171. . ') AS ' . Util::backquote(
  172. $this->_userSpecifiedSettings['spatialColumn']
  173. )
  174. . ', ';
  175. // Get the SRID
  176. $modified_query .= $spatialSrid . '('
  177. . Util::backquote($this->_userSpecifiedSettings['spatialColumn'])
  178. . ') AS ' . Util::backquote('srid') . ' ';
  179. // Append the original query as the inner query
  180. $modified_query .= 'FROM (' . $sql_query . ') AS '
  181. . Util::backquote('temp_gis');
  182. // LIMIT clause
  183. if (is_numeric($rows) && $rows > 0) {
  184. $modified_query .= ' LIMIT ';
  185. if (is_numeric($pos) && $pos >= 0) {
  186. $modified_query .= $pos . ', ' . $rows;
  187. } else {
  188. $modified_query .= $rows;
  189. }
  190. }
  191. return $modified_query;
  192. }
  193. /**
  194. * Returns raw data for GIS visualization.
  195. *
  196. * @return string the raw data.
  197. */
  198. private function _fetchRawData()
  199. {
  200. $modified_result = $GLOBALS['dbi']->tryQuery($this->_modified_sql);
  201. if ($modified_result === false) {
  202. return array();
  203. }
  204. $data = array();
  205. while ($row = $GLOBALS['dbi']->fetchAssoc($modified_result)) {
  206. $data[] = $row;
  207. }
  208. return $data;
  209. }
  210. /**
  211. * A function which handles passed parameters. Useful if desired
  212. * chart needs to be a little bit different from the default one.
  213. *
  214. * @return void
  215. * @access private
  216. */
  217. private function _handleOptions()
  218. {
  219. if (!is_null($this->_userSpecifiedSettings)) {
  220. $this->_settings = array_merge(
  221. $this->_settings,
  222. $this->_userSpecifiedSettings
  223. );
  224. }
  225. }
  226. /**
  227. * Sanitizes the file name.
  228. *
  229. * @param string $file_name file name
  230. * @param string $ext extension of the file
  231. *
  232. * @return string the sanitized file name
  233. * @access private
  234. */
  235. private function _sanitizeName($file_name, $ext)
  236. {
  237. $file_name = Sanitize::sanitizeFilename($file_name);
  238. // Check if the user already added extension;
  239. // get the substring where the extension would be if it was included
  240. $extension_start_pos = mb_strlen($file_name) - mb_strlen($ext) - 1;
  241. $user_extension
  242. = mb_substr(
  243. $file_name,
  244. $extension_start_pos,
  245. mb_strlen($file_name)
  246. );
  247. $required_extension = "." . $ext;
  248. if (mb_strtolower($user_extension) != $required_extension) {
  249. $file_name .= $required_extension;
  250. }
  251. return $file_name;
  252. }
  253. /**
  254. * Handles common tasks of writing the visualization to file for various formats.
  255. *
  256. * @param string $file_name file name
  257. * @param string $type mime type
  258. * @param string $ext extension of the file
  259. *
  260. * @return void
  261. * @access private
  262. */
  263. private function _toFile($file_name, $type, $ext)
  264. {
  265. $file_name = $this->_sanitizeName($file_name, $ext);
  266. Core::downloadHeader($file_name, $type);
  267. }
  268. /**
  269. * Generate the visualization in SVG format.
  270. *
  271. * @return string the generated image resource
  272. * @access private
  273. */
  274. private function _svg()
  275. {
  276. $this->init();
  277. $output = '<?xml version="1.0" encoding="UTF-8" standalone="no"?' . ' >'
  278. . "\n"
  279. . '<svg version="1.1" xmlns:svg="http://www.w3.org/2000/svg"'
  280. . ' xmlns="http://www.w3.org/2000/svg"'
  281. . ' width="' . intval($this->_settings['width']) . '"'
  282. . ' height="' . intval($this->_settings['height']) . '">'
  283. . '<g id="groupPanel">';
  284. $scale_data = $this->_scaleDataSet($this->_data);
  285. $output .= $this->_prepareDataSet($this->_data, $scale_data, 'svg', '');
  286. $output .= '</g></svg>';
  287. return $output;
  288. }
  289. /**
  290. * Get the visualization as a SVG.
  291. *
  292. * @return string the visualization as a SVG
  293. * @access public
  294. */
  295. public function asSVG()
  296. {
  297. $output = $this->_svg();
  298. return $output;
  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. '', 'pt', $GLOBALS['cfg']['PDFDefaultPageSize'], true, 'UTF-8', false
  438. );
  439. // disable header and footer
  440. $pdf->setPrintHeader(false);
  441. $pdf->setPrintFooter(false);
  442. //set auto page breaks
  443. $pdf->SetAutoPageBreak(false);
  444. // add a page
  445. $pdf->AddPage();
  446. $scale_data = $this->_scaleDataSet($this->_data);
  447. $pdf = $this->_prepareDataSet($this->_data, $scale_data, 'pdf', $pdf);
  448. // sanitize file name
  449. $file_name = $this->_sanitizeName($file_name, 'pdf');
  450. $pdf->Output($file_name, 'D');
  451. }
  452. /**
  453. * Convert file to image
  454. *
  455. * @param string $format Output format
  456. *
  457. * @return string File
  458. */
  459. public function toImage($format)
  460. {
  461. if ($format == 'svg') {
  462. return $this->asSvg();
  463. } elseif ($format == 'png') {
  464. return $this->asPng();
  465. } elseif ($format == 'ol') {
  466. return $this->asOl();
  467. }
  468. }
  469. /**
  470. * Convert file to given format
  471. *
  472. * @param string $filename Filename
  473. * @param string $format Output format
  474. *
  475. * @return void
  476. */
  477. public function toFile($filename, $format)
  478. {
  479. if ($format == 'svg') {
  480. $this->toFileAsSvg($filename);
  481. } elseif ($format == 'png') {
  482. $this->toFileAsPng($filename);
  483. } elseif ($format == 'pdf') {
  484. $this->toFileAsPdf($filename);
  485. }
  486. }
  487. /**
  488. * Calculates the scale, horizontal and vertical offset that should be used.
  489. *
  490. * @param array $data Row data
  491. *
  492. * @return array an array containing the scale, x and y offsets
  493. * @access private
  494. */
  495. private function _scaleDataSet(array $data)
  496. {
  497. $min_max = array(
  498. 'maxX' => 0.0,
  499. 'maxY' => 0.0,
  500. 'minX' => 0.0,
  501. 'minY' => 0.0
  502. );
  503. $border = 15;
  504. // effective width and height of the plot
  505. $plot_width = $this->_settings['width'] - 2 * $border;
  506. $plot_height = $this->_settings['height'] - 2 * $border;
  507. foreach ($data as $row) {
  508. // Figure out the data type
  509. $ref_data = $row[$this->_settings['spatialColumn']];
  510. $type_pos = mb_strpos($ref_data, '(');
  511. if ($type_pos === false) {
  512. continue;
  513. }
  514. $type = mb_substr($ref_data, 0, $type_pos);
  515. $gis_obj = GisFactory::factory($type);
  516. if (!$gis_obj) {
  517. continue;
  518. }
  519. $scale_data = $gis_obj->scaleRow(
  520. $row[$this->_settings['spatialColumn']]
  521. );
  522. // Update minimum/maximum values for x and y coordinates.
  523. $c_maxX = (float)$scale_data['maxX'];
  524. if ($min_max['maxX'] === 0.0 || $c_maxX > $min_max['maxX']) {
  525. $min_max['maxX'] = $c_maxX;
  526. }
  527. $c_minX = (float)$scale_data['minX'];
  528. if ($min_max['minX'] === 0.0 || $c_minX < $min_max['minX']) {
  529. $min_max['minX'] = $c_minX;
  530. }
  531. $c_maxY = (float)$scale_data['maxY'];
  532. if ($min_max['maxY'] === 0.0 || $c_maxY > $min_max['maxY']) {
  533. $min_max['maxY'] = $c_maxY;
  534. }
  535. $c_minY = (float)$scale_data['minY'];
  536. if ($min_max['minY'] === 0.0 || $c_minY < $min_max['minY']) {
  537. $min_max['minY'] = $c_minY;
  538. }
  539. }
  540. // scale the visualization
  541. $x_ratio = ($min_max['maxX'] - $min_max['minX']) / $plot_width;
  542. $y_ratio = ($min_max['maxY'] - $min_max['minY']) / $plot_height;
  543. $ratio = ($x_ratio > $y_ratio) ? $x_ratio : $y_ratio;
  544. $scale = ($ratio != 0) ? (1 / $ratio) : 1;
  545. if ($x_ratio < $y_ratio) {
  546. // center horizontally
  547. $x = ($min_max['maxX'] + $min_max['minX'] - $plot_width / $scale) / 2;
  548. // fit vertically
  549. $y = $min_max['minY'] - ($border / $scale);
  550. } else {
  551. // fit horizontally
  552. $x = $min_max['minX'] - ($border / $scale);
  553. // center vertically
  554. $y = ($min_max['maxY'] + $min_max['minY'] - $plot_height / $scale) / 2;
  555. }
  556. return array(
  557. 'scale' => $scale,
  558. 'x' => $x,
  559. 'y' => $y,
  560. 'minX' => $min_max['minX'],
  561. 'maxX' => $min_max['maxX'],
  562. 'minY' => $min_max['minY'],
  563. 'maxY' => $min_max['maxY'],
  564. 'height' => $this->_settings['height'],
  565. );
  566. }
  567. /**
  568. * Prepares and return the dataset as needed by the visualization.
  569. *
  570. * @param array $data Raw data
  571. * @param array $scale_data Data related to scaling
  572. * @param string $format Format of the visualization
  573. * @param object $results Image object in the case of png
  574. * TCPDF object in the case of pdf
  575. *
  576. * @return mixed the formatted array of data
  577. * @access private
  578. */
  579. private function _prepareDataSet(array $data, array $scale_data, $format, $results)
  580. {
  581. $color_number = 0;
  582. // loop through the rows
  583. foreach ($data as $row) {
  584. $index = $color_number % sizeof($this->_settings['colors']);
  585. // Figure out the data type
  586. $ref_data = $row[$this->_settings['spatialColumn']];
  587. $type_pos = mb_strpos($ref_data, '(');
  588. if ($type_pos === false) {
  589. continue;
  590. }
  591. $type = mb_substr($ref_data, 0, $type_pos);
  592. $gis_obj = GisFactory::factory($type);
  593. if (!$gis_obj) {
  594. continue;
  595. }
  596. $label = '';
  597. if (isset($this->_settings['labelColumn'])
  598. && isset($row[$this->_settings['labelColumn']])
  599. ) {
  600. $label = $row[$this->_settings['labelColumn']];
  601. }
  602. if ($format == 'svg') {
  603. $results .= $gis_obj->prepareRowAsSvg(
  604. $row[$this->_settings['spatialColumn']],
  605. $label,
  606. $this->_settings['colors'][$index],
  607. $scale_data
  608. );
  609. } elseif ($format == 'png') {
  610. $results = $gis_obj->prepareRowAsPng(
  611. $row[$this->_settings['spatialColumn']],
  612. $label,
  613. $this->_settings['colors'][$index],
  614. $scale_data,
  615. $results
  616. );
  617. } elseif ($format == 'pdf') {
  618. $results = $gis_obj->prepareRowAsPdf(
  619. $row[$this->_settings['spatialColumn']],
  620. $label,
  621. $this->_settings['colors'][$index],
  622. $scale_data,
  623. $results
  624. );
  625. } elseif ($format == 'ol') {
  626. $results .= $gis_obj->prepareRowAsOl(
  627. $row[$this->_settings['spatialColumn']],
  628. $row['srid'],
  629. $label,
  630. $this->_settings['colors'][$index],
  631. $scale_data
  632. );
  633. }
  634. $color_number++;
  635. }
  636. return $results;
  637. }
  638. /**
  639. * Set user specified settings
  640. *
  641. * @param array $userSpecifiedSettings User specified settings
  642. *
  643. * @return void
  644. */
  645. public function setUserSpecifiedSettings(array $userSpecifiedSettings)
  646. {
  647. $this->_userSpecifiedSettings = $userSpecifiedSettings;
  648. }
  649. }