GisVisualization.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  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. // If label column is chosen add it to the query
  156. if (!empty($this->_userSpecifiedSettings['labelColumn'])) {
  157. $modified_query .= Util::backquote(
  158. $this->_userSpecifiedSettings['labelColumn']
  159. )
  160. . ', ';
  161. }
  162. // Wrap the spatial column with 'ASTEXT()' function and add it
  163. $modified_query .= 'ASTEXT('
  164. . Util::backquote($this->_userSpecifiedSettings['spatialColumn'])
  165. . ') AS ' . Util::backquote(
  166. $this->_userSpecifiedSettings['spatialColumn']
  167. )
  168. . ', ';
  169. // Get the SRID
  170. $modified_query .= 'SRID('
  171. . Util::backquote($this->_userSpecifiedSettings['spatialColumn'])
  172. . ') AS ' . Util::backquote('srid') . ' ';
  173. // Append the original query as the inner query
  174. $modified_query .= 'FROM (' . $sql_query . ') AS '
  175. . Util::backquote('temp_gis');
  176. // LIMIT clause
  177. if (is_numeric($rows) && $rows > 0) {
  178. $modified_query .= ' LIMIT ';
  179. if (is_numeric($pos) && $pos >= 0) {
  180. $modified_query .= $pos . ', ' . $rows;
  181. } else {
  182. $modified_query .= $rows;
  183. }
  184. }
  185. return $modified_query;
  186. }
  187. /**
  188. * Returns raw data for GIS visualization.
  189. *
  190. * @return string the raw data.
  191. */
  192. private function _fetchRawData()
  193. {
  194. $modified_result = $GLOBALS['dbi']->tryQuery($this->_modified_sql);
  195. if ($modified_result === false) {
  196. return array();
  197. }
  198. $data = array();
  199. while ($row = $GLOBALS['dbi']->fetchAssoc($modified_result)) {
  200. $data[] = $row;
  201. }
  202. return $data;
  203. }
  204. /**
  205. * A function which handles passed parameters. Useful if desired
  206. * chart needs to be a little bit different from the default one.
  207. *
  208. * @return void
  209. * @access private
  210. */
  211. private function _handleOptions()
  212. {
  213. if (!is_null($this->_userSpecifiedSettings)) {
  214. $this->_settings = array_merge(
  215. $this->_settings,
  216. $this->_userSpecifiedSettings
  217. );
  218. }
  219. }
  220. /**
  221. * Sanitizes the file name.
  222. *
  223. * @param string $file_name file name
  224. * @param string $ext extension of the file
  225. *
  226. * @return string the sanitized file name
  227. * @access private
  228. */
  229. private function _sanitizeName($file_name, $ext)
  230. {
  231. $file_name = Sanitize::sanitizeFilename($file_name);
  232. // Check if the user already added extension;
  233. // get the substring where the extension would be if it was included
  234. $extension_start_pos = mb_strlen($file_name) - mb_strlen($ext) - 1;
  235. $user_extension
  236. = mb_substr(
  237. $file_name,
  238. $extension_start_pos,
  239. mb_strlen($file_name)
  240. );
  241. $required_extension = "." . $ext;
  242. if (mb_strtolower($user_extension) != $required_extension) {
  243. $file_name .= $required_extension;
  244. }
  245. return $file_name;
  246. }
  247. /**
  248. * Handles common tasks of writing the visualization to file for various formats.
  249. *
  250. * @param string $file_name file name
  251. * @param string $type mime type
  252. * @param string $ext extension of the file
  253. *
  254. * @return void
  255. * @access private
  256. */
  257. private function _toFile($file_name, $type, $ext)
  258. {
  259. $file_name = $this->_sanitizeName($file_name, $ext);
  260. Core::downloadHeader($file_name, $type);
  261. }
  262. /**
  263. * Generate the visualization in SVG format.
  264. *
  265. * @return string the generated image resource
  266. * @access private
  267. */
  268. private function _svg()
  269. {
  270. $this->init();
  271. $output = '<?xml version="1.0" encoding="UTF-8" standalone="no"?' . ' >'
  272. . "\n"
  273. . '<svg version="1.1" xmlns:svg="http://www.w3.org/2000/svg"'
  274. . ' xmlns="http://www.w3.org/2000/svg"'
  275. . ' width="' . intval($this->_settings['width']) . '"'
  276. . ' height="' . intval($this->_settings['height']) . '">'
  277. . '<g id="groupPanel">';
  278. $scale_data = $this->_scaleDataSet($this->_data);
  279. $output .= $this->_prepareDataSet($this->_data, $scale_data, 'svg', '');
  280. $output .= '</g></svg>';
  281. return $output;
  282. }
  283. /**
  284. * Get the visualization as a SVG.
  285. *
  286. * @return string the visualization as a SVG
  287. * @access public
  288. */
  289. public function asSVG()
  290. {
  291. $output = $this->_svg();
  292. return $output;
  293. }
  294. /**
  295. * Saves as a SVG image to a file.
  296. *
  297. * @param string $file_name File name
  298. *
  299. * @return void
  300. * @access public
  301. */
  302. public function toFileAsSvg($file_name)
  303. {
  304. $img = $this->_svg();
  305. $this->_toFile($file_name, 'image/svg+xml', 'svg');
  306. echo($img);
  307. }
  308. /**
  309. * Generate the visualization in PNG format.
  310. *
  311. * @return resource the generated image resource
  312. * @access private
  313. */
  314. private function _png()
  315. {
  316. $this->init();
  317. // create image
  318. $image = imagecreatetruecolor(
  319. $this->_settings['width'],
  320. $this->_settings['height']
  321. );
  322. // fill the background
  323. $bg = imagecolorallocate($image, 229, 229, 229);
  324. imagefilledrectangle(
  325. $image,
  326. 0,
  327. 0,
  328. $this->_settings['width'] - 1,
  329. $this->_settings['height'] - 1,
  330. $bg
  331. );
  332. $scale_data = $this->_scaleDataSet($this->_data);
  333. $image = $this->_prepareDataSet($this->_data, $scale_data, 'png', $image);
  334. return $image;
  335. }
  336. /**
  337. * Get the visualization as a PNG.
  338. *
  339. * @return string the visualization as a PNG
  340. * @access public
  341. */
  342. public function asPng()
  343. {
  344. $img = $this->_png();
  345. // render and save it to variable
  346. ob_start();
  347. imagepng($img, null, 9, PNG_ALL_FILTERS);
  348. imagedestroy($img);
  349. $output = ob_get_contents();
  350. ob_end_clean();
  351. // base64 encode
  352. $encoded = base64_encode($output);
  353. return '<img src="data:image/png;base64,' . $encoded . '" />';
  354. }
  355. /**
  356. * Saves as a PNG image to a file.
  357. *
  358. * @param string $file_name File name
  359. *
  360. * @return void
  361. * @access public
  362. */
  363. public function toFileAsPng($file_name)
  364. {
  365. $img = $this->_png();
  366. $this->_toFile($file_name, 'image/png', 'png');
  367. imagepng($img, null, 9, PNG_ALL_FILTERS);
  368. imagedestroy($img);
  369. }
  370. /**
  371. * Get the code for visualization with OpenLayers.
  372. *
  373. * @todo Should return JSON to avoid eval() in gis_data_editor.js
  374. *
  375. * @return string the code for visualization with OpenLayers
  376. * @access public
  377. */
  378. public function asOl()
  379. {
  380. $this->init();
  381. $scale_data = $this->_scaleDataSet($this->_data);
  382. $output
  383. = 'if (typeof OpenLayers !== "undefined") {'
  384. . 'var options = {'
  385. . 'projection: new OpenLayers.Projection("EPSG:900913"),'
  386. . 'displayProjection: new OpenLayers.Projection("EPSG:4326"),'
  387. . 'units: "m",'
  388. . 'numZoomLevels: 18,'
  389. . 'maxResolution: 156543.0339,'
  390. . 'maxExtent: new OpenLayers.Bounds('
  391. . '-20037508, -20037508, 20037508, 20037508),'
  392. . 'restrictedExtent: new OpenLayers.Bounds('
  393. . '-20037508, -20037508, 20037508, 20037508)'
  394. . '};'
  395. . 'var map = new OpenLayers.Map("openlayersmap", options);'
  396. . 'var layerNone = new OpenLayers.Layer.Boxes('
  397. . '"None", {isBaseLayer: true});'
  398. . 'var layerOSM = new OpenLayers.Layer.OSM("OSM",'
  399. . '['
  400. . '"https://a.tile.openstreetmap.org/${z}/${x}/${y}.png",'
  401. . '"https://b.tile.openstreetmap.org/${z}/${x}/${y}.png",'
  402. . '"https://c.tile.openstreetmap.org/${z}/${x}/${y}.png"'
  403. . ']);'
  404. . 'map.addLayers([layerOSM,layerNone]);'
  405. . 'var vectorLayer = new OpenLayers.Layer.Vector("Data");'
  406. . 'var bound;';
  407. $output .= $this->_prepareDataSet($this->_data, $scale_data, 'ol', '');
  408. $output .= 'map.addLayer(vectorLayer);'
  409. . 'map.zoomToExtent(bound);'
  410. . 'if (map.getZoom() < 2) {'
  411. . 'map.zoomTo(2);'
  412. . '}'
  413. . 'map.addControl(new OpenLayers.Control.LayerSwitcher());'
  414. . 'map.addControl(new OpenLayers.Control.MousePosition());'
  415. . '}';
  416. return $output;
  417. }
  418. /**
  419. * Saves as a PDF to a file.
  420. *
  421. * @param string $file_name File name
  422. *
  423. * @return void
  424. * @access public
  425. */
  426. public function toFileAsPdf($file_name)
  427. {
  428. $this->init();
  429. // create pdf
  430. $pdf = new TCPDF(
  431. '', 'pt', $GLOBALS['cfg']['PDFDefaultPageSize'], true, 'UTF-8', false
  432. );
  433. // disable header and footer
  434. $pdf->setPrintHeader(false);
  435. $pdf->setPrintFooter(false);
  436. //set auto page breaks
  437. $pdf->SetAutoPageBreak(false);
  438. // add a page
  439. $pdf->AddPage();
  440. $scale_data = $this->_scaleDataSet($this->_data);
  441. $pdf = $this->_prepareDataSet($this->_data, $scale_data, 'pdf', $pdf);
  442. // sanitize file name
  443. $file_name = $this->_sanitizeName($file_name, 'pdf');
  444. $pdf->Output($file_name, 'D');
  445. }
  446. /**
  447. * Convert file to image
  448. *
  449. * @param string $format Output format
  450. *
  451. * @return string File
  452. */
  453. public function toImage($format)
  454. {
  455. if ($format == 'svg') {
  456. return $this->asSvg();
  457. } elseif ($format == 'png') {
  458. return $this->asPng();
  459. } elseif ($format == 'ol') {
  460. return $this->asOl();
  461. }
  462. }
  463. /**
  464. * Convert file to given format
  465. *
  466. * @param string $filename Filename
  467. * @param string $format Output format
  468. *
  469. * @return void
  470. */
  471. public function toFile($filename, $format)
  472. {
  473. if ($format == 'svg') {
  474. $this->toFileAsSvg($filename);
  475. } elseif ($format == 'png') {
  476. $this->toFileAsPng($filename);
  477. } elseif ($format == 'pdf') {
  478. $this->toFileAsPdf($filename);
  479. }
  480. }
  481. /**
  482. * Calculates the scale, horizontal and vertical offset that should be used.
  483. *
  484. * @param array $data Row data
  485. *
  486. * @return array an array containing the scale, x and y offsets
  487. * @access private
  488. */
  489. private function _scaleDataSet(array $data)
  490. {
  491. $min_max = array();
  492. $border = 15;
  493. // effective width and height of the plot
  494. $plot_width = $this->_settings['width'] - 2 * $border;
  495. $plot_height = $this->_settings['height'] - 2 * $border;
  496. foreach ($data as $row) {
  497. // Figure out the data type
  498. $ref_data = $row[$this->_settings['spatialColumn']];
  499. $type_pos = mb_strpos($ref_data, '(');
  500. if ($type_pos === false) {
  501. continue;
  502. }
  503. $type = mb_substr($ref_data, 0, $type_pos);
  504. $gis_obj = GisFactory::factory($type);
  505. if (!$gis_obj) {
  506. continue;
  507. }
  508. $scale_data = $gis_obj->scaleRow(
  509. $row[$this->_settings['spatialColumn']]
  510. );
  511. // Update minimum/maximum values for x and y coordinates.
  512. $c_maxX = (float)$scale_data['maxX'];
  513. if (!isset($min_max['maxX']) || $c_maxX > $min_max['maxX']) {
  514. $min_max['maxX'] = $c_maxX;
  515. }
  516. $c_minX = (float)$scale_data['minX'];
  517. if (!isset($min_max['minX']) || $c_minX < $min_max['minX']) {
  518. $min_max['minX'] = $c_minX;
  519. }
  520. $c_maxY = (float)$scale_data['maxY'];
  521. if (!isset($min_max['maxY']) || $c_maxY > $min_max['maxY']) {
  522. $min_max['maxY'] = $c_maxY;
  523. }
  524. $c_minY = (float)$scale_data['minY'];
  525. if (!isset($min_max['minY']) || $c_minY < $min_max['minY']) {
  526. $min_max['minY'] = $c_minY;
  527. }
  528. }
  529. // scale the visualization
  530. $x_ratio = ($min_max['maxX'] - $min_max['minX']) / $plot_width;
  531. $y_ratio = ($min_max['maxY'] - $min_max['minY']) / $plot_height;
  532. $ratio = ($x_ratio > $y_ratio) ? $x_ratio : $y_ratio;
  533. $scale = ($ratio != 0) ? (1 / $ratio) : 1;
  534. if ($x_ratio < $y_ratio) {
  535. // center horizontally
  536. $x = ($min_max['maxX'] + $min_max['minX'] - $plot_width / $scale) / 2;
  537. // fit vertically
  538. $y = $min_max['minY'] - ($border / $scale);
  539. } else {
  540. // fit horizontally
  541. $x = $min_max['minX'] - ($border / $scale);
  542. // center vertically
  543. $y = ($min_max['maxY'] + $min_max['minY'] - $plot_height / $scale) / 2;
  544. }
  545. return array(
  546. 'scale' => $scale,
  547. 'x' => $x,
  548. 'y' => $y,
  549. 'minX' => $min_max['minX'],
  550. 'maxX' => $min_max['maxX'],
  551. 'minY' => $min_max['minY'],
  552. 'maxY' => $min_max['maxY'],
  553. 'height' => $this->_settings['height'],
  554. );
  555. }
  556. /**
  557. * Prepares and return the dataset as needed by the visualization.
  558. *
  559. * @param array $data Raw data
  560. * @param array $scale_data Data related to scaling
  561. * @param string $format Format of the visualization
  562. * @param object $results Image object in the case of png
  563. * TCPDF object in the case of pdf
  564. *
  565. * @return mixed the formatted array of data
  566. * @access private
  567. */
  568. private function _prepareDataSet(array $data, array $scale_data, $format, $results)
  569. {
  570. $color_number = 0;
  571. // loop through the rows
  572. foreach ($data as $row) {
  573. $index = $color_number % sizeof($this->_settings['colors']);
  574. // Figure out the data type
  575. $ref_data = $row[$this->_settings['spatialColumn']];
  576. $type_pos = mb_strpos($ref_data, '(');
  577. if ($type_pos === false) {
  578. continue;
  579. }
  580. $type = mb_substr($ref_data, 0, $type_pos);
  581. $gis_obj = GisFactory::factory($type);
  582. if (!$gis_obj) {
  583. continue;
  584. }
  585. $label = '';
  586. if (isset($this->_settings['labelColumn'])
  587. && isset($row[$this->_settings['labelColumn']])
  588. ) {
  589. $label = $row[$this->_settings['labelColumn']];
  590. }
  591. if ($format == 'svg') {
  592. $results .= $gis_obj->prepareRowAsSvg(
  593. $row[$this->_settings['spatialColumn']],
  594. $label,
  595. $this->_settings['colors'][$index],
  596. $scale_data
  597. );
  598. } elseif ($format == 'png') {
  599. $results = $gis_obj->prepareRowAsPng(
  600. $row[$this->_settings['spatialColumn']],
  601. $label,
  602. $this->_settings['colors'][$index],
  603. $scale_data,
  604. $results
  605. );
  606. } elseif ($format == 'pdf') {
  607. $results = $gis_obj->prepareRowAsPdf(
  608. $row[$this->_settings['spatialColumn']],
  609. $label,
  610. $this->_settings['colors'][$index],
  611. $scale_data,
  612. $results
  613. );
  614. } elseif ($format == 'ol') {
  615. $results .= $gis_obj->prepareRowAsOl(
  616. $row[$this->_settings['spatialColumn']],
  617. $row['srid'],
  618. $label,
  619. $this->_settings['colors'][$index],
  620. $scale_data
  621. );
  622. }
  623. $color_number++;
  624. }
  625. return $results;
  626. }
  627. /**
  628. * Set user specified settings
  629. *
  630. * @param array $userSpecifiedSettings User specified settings
  631. *
  632. * @return void
  633. */
  634. public function setUserSpecifiedSettings(array $userSpecifiedSettings)
  635. {
  636. $this->_userSpecifiedSettings = $userSpecifiedSettings;
  637. }
  638. }