GIS_Polygon.class.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. <?php
  2. /* vim: set expandtab sw=4 ts=4 sts=4: */
  3. /**
  4. * Handles actions related to GIS POLYGON objects
  5. *
  6. * @package PhpMyAdmin-GIS
  7. */
  8. if (! defined('PHPMYADMIN')) {
  9. exit;
  10. }
  11. /**
  12. * Handles actions related to GIS POLYGON objects
  13. *
  14. * @package PhpMyAdmin-GIS
  15. */
  16. class PMA_GIS_Polygon extends PMA_GIS_Geometry
  17. {
  18. // Hold the singleton instance of the class
  19. private static $_instance;
  20. /**
  21. * A private constructor; prevents direct creation of object.
  22. *
  23. * @access private
  24. */
  25. private function __construct()
  26. {
  27. }
  28. /**
  29. * Returns the singleton.
  30. *
  31. * @return PMA_GIS_Polygon the singleton
  32. * @access public
  33. */
  34. public static function singleton()
  35. {
  36. if (!isset(self::$_instance)) {
  37. $class = __CLASS__;
  38. self::$_instance = new $class;
  39. }
  40. return self::$_instance;
  41. }
  42. /**
  43. * Scales each row.
  44. *
  45. * @param string $spatial spatial data of a row
  46. *
  47. * @return array an array containing the min, max values for x and y cordinates
  48. * @access public
  49. */
  50. public function scaleRow($spatial)
  51. {
  52. // Trim to remove leading 'POLYGON((' and trailing '))'
  53. $polygon = substr($spatial, 9, (strlen($spatial) - 11));
  54. // If the polygon doesn't have an inner ring, use polygon itself
  55. if (strpos($polygon, "),(") === false) {
  56. $ring = $polygon;
  57. } else {
  58. // Seperate outer ring and use it to determin min-max
  59. $parts = explode("),(", $polygon);
  60. $ring = $parts[0];
  61. }
  62. return $this->setMinMax($ring, array());
  63. }
  64. /**
  65. * Adds to the PNG image object, the data related to a row in the GIS dataset.
  66. *
  67. * @param string $spatial GIS POLYGON object
  68. * @param string $label Label for the GIS POLYGON object
  69. * @param string $fill_color Color for the GIS POLYGON object
  70. * @param array $scale_data Array containing data related to scaling
  71. * @param resource $image Image object
  72. *
  73. * @return object the modified image object
  74. * @access public
  75. */
  76. public function prepareRowAsPng($spatial, $label, $fill_color,
  77. $scale_data, $image
  78. ) {
  79. // allocate colors
  80. $black = imagecolorallocate($image, 0, 0, 0);
  81. $red = hexdec(substr($fill_color, 1, 2));
  82. $green = hexdec(substr($fill_color, 3, 2));
  83. $blue = hexdec(substr($fill_color, 4, 2));
  84. $color = imagecolorallocate($image, $red, $green, $blue);
  85. // Trim to remove leading 'POLYGON((' and trailing '))'
  86. $polygon = substr($spatial, 9, (strlen($spatial) - 11));
  87. // If the polygon doesnt have an inner polygon
  88. if (strpos($polygon, "),(") === false) {
  89. $points_arr = $this->extractPoints($polygon, $scale_data, true);
  90. } else {
  91. // Seperate outer and inner polygons
  92. $parts = explode("),(", $polygon);
  93. $outer = $parts[0];
  94. $inner = array_slice($parts, 1);
  95. $points_arr = $this->extractPoints($outer, $scale_data, true);
  96. foreach ($inner as $inner_poly) {
  97. $points_arr = array_merge(
  98. $points_arr, $this->extractPoints($inner_poly, $scale_data, true)
  99. );
  100. }
  101. }
  102. // draw polygon
  103. imagefilledpolygon($image, $points_arr, sizeof($points_arr) / 2, $color);
  104. // print label if applicable
  105. if (isset($label) && trim($label) != '') {
  106. imagestring(
  107. $image, 1, $points_arr[2], $points_arr[3], trim($label), $black
  108. );
  109. }
  110. return $image;
  111. }
  112. /**
  113. * Adds to the TCPDF instance, the data related to a row in the GIS dataset.
  114. *
  115. * @param string $spatial GIS POLYGON object
  116. * @param string $label Label for the GIS POLYGON object
  117. * @param string $fill_color Color for the GIS POLYGON object
  118. * @param array $scale_data Array containing data related to scaling
  119. * @param TCPDF $pdf TCPDF instance
  120. *
  121. * @return TCPDF the modified TCPDF instance
  122. * @access public
  123. */
  124. public function prepareRowAsPdf($spatial, $label, $fill_color, $scale_data, $pdf)
  125. {
  126. // allocate colors
  127. $red = hexdec(substr($fill_color, 1, 2));
  128. $green = hexdec(substr($fill_color, 3, 2));
  129. $blue = hexdec(substr($fill_color, 4, 2));
  130. $color = array($red, $green, $blue);
  131. // Trim to remove leading 'POLYGON((' and trailing '))'
  132. $polygon = substr($spatial, 9, (strlen($spatial) - 11));
  133. // If the polygon doesnt have an inner polygon
  134. if (strpos($polygon, "),(") === false) {
  135. $points_arr = $this->extractPoints($polygon, $scale_data, true);
  136. } else {
  137. // Seperate outer and inner polygons
  138. $parts = explode("),(", $polygon);
  139. $outer = $parts[0];
  140. $inner = array_slice($parts, 1);
  141. $points_arr = $this->extractPoints($outer, $scale_data, true);
  142. foreach ($inner as $inner_poly) {
  143. $points_arr = array_merge(
  144. $points_arr, $this->extractPoints($inner_poly, $scale_data, true)
  145. );
  146. }
  147. }
  148. // draw polygon
  149. $pdf->Polygon($points_arr, 'F*', array(), $color, true);
  150. // print label if applicable
  151. if (isset($label) && trim($label) != '') {
  152. $pdf->SetXY($points_arr[2], $points_arr[3]);
  153. $pdf->SetFontSize(5);
  154. $pdf->Cell(0, 0, trim($label));
  155. }
  156. return $pdf;
  157. }
  158. /**
  159. * Prepares and returns the code related to a row in the GIS dataset as SVG.
  160. *
  161. * @param string $spatial GIS POLYGON object
  162. * @param string $label Label for the GIS POLYGON object
  163. * @param string $fill_color Color for the GIS POLYGON object
  164. * @param array $scale_data Array containing data related to scaling
  165. *
  166. * @return string the code related to a row in the GIS dataset
  167. * @access public
  168. */
  169. public function prepareRowAsSvg($spatial, $label, $fill_color, $scale_data)
  170. {
  171. $polygon_options = array(
  172. 'name' => $label,
  173. 'id' => $label . rand(),
  174. 'class' => 'polygon vector',
  175. 'stroke' => 'black',
  176. 'stroke-width'=> 0.5,
  177. 'fill' => $fill_color,
  178. 'fill-rule' => 'evenodd',
  179. 'fill-opacity'=> 0.8,
  180. );
  181. // Trim to remove leading 'POLYGON((' and trailing '))'
  182. $polygon = substr($spatial, 9, (strlen($spatial) - 11));
  183. $row = '<path d="';
  184. // If the polygon doesnt have an inner polygon
  185. if (strpos($polygon, "),(") === false) {
  186. $row .= $this->_drawPath($polygon, $scale_data);
  187. } else {
  188. // Seperate outer and inner polygons
  189. $parts = explode("),(", $polygon);
  190. $outer = $parts[0];
  191. $inner = array_slice($parts, 1);
  192. $row .= $this->_drawPath($outer, $scale_data);
  193. foreach ($inner as $inner_poly) {
  194. $row .= $this->_drawPath($inner_poly, $scale_data);
  195. }
  196. }
  197. $row .= '"';
  198. foreach ($polygon_options as $option => $val) {
  199. $row .= ' ' . $option . '="' . trim($val) . '"';
  200. }
  201. $row .= '/>';
  202. return $row;
  203. }
  204. /**
  205. * Prepares JavaScript related to a row in the GIS dataset
  206. * to visualize it with OpenLayers.
  207. *
  208. * @param string $spatial GIS POLYGON object
  209. * @param int $srid Spatial reference ID
  210. * @param string $label Label for the GIS POLYGON object
  211. * @param string $fill_color Color for the GIS POLYGON object
  212. * @param array $scale_data Array containing data related to scaling
  213. *
  214. * @return string JavaScript related to a row in the GIS dataset
  215. * @access public
  216. */
  217. public function prepareRowAsOl($spatial, $srid, $label, $fill_color, $scale_data)
  218. {
  219. $style_options = array(
  220. 'strokeColor' => '#000000',
  221. 'strokeWidth' => 0.5,
  222. 'fillColor' => $fill_color,
  223. 'fillOpacity' => 0.8,
  224. 'label' => $label,
  225. 'fontSize' => 10,
  226. );
  227. if ($srid == 0) {
  228. $srid = 4326;
  229. }
  230. $row = $this->getBoundsForOl($srid, $scale_data);
  231. // Trim to remove leading 'POLYGON((' and trailing '))'
  232. $polygon = substr($spatial, 9, (strlen($spatial) - 11));
  233. // Seperate outer and inner polygons
  234. $parts = explode("),(", $polygon);
  235. $row .= 'vectorLayer.addFeatures(new OpenLayers.Feature.Vector('
  236. . $this->getPolygonForOpenLayers($parts, $srid)
  237. . ', null, ' . json_encode($style_options) . '));';
  238. return $row;
  239. }
  240. /**
  241. * Draws a ring of the polygon using SVG path element.
  242. *
  243. * @param string $polygon The ring
  244. * @param array $scale_data Array containing data related to scaling
  245. *
  246. * @return string the code to draw the ring
  247. * @access private
  248. */
  249. private function _drawPath($polygon, $scale_data)
  250. {
  251. $points_arr = $this->extractPoints($polygon, $scale_data);
  252. $row = ' M ' . $points_arr[0][0] . ', ' . $points_arr[0][1];
  253. $other_points = array_slice($points_arr, 1, count($points_arr) - 2);
  254. foreach ($other_points as $point) {
  255. $row .= ' L ' . $point[0] . ', ' . $point[1];
  256. }
  257. $row .= ' Z ';
  258. return $row;
  259. }
  260. /**
  261. * Generate the WKT with the set of parameters passed by the GIS editor.
  262. *
  263. * @param array $gis_data GIS data
  264. * @param int $index Index into the parameter object
  265. * @param string $empty Value for empty points
  266. *
  267. * @return string WKT with the set of parameters passed by the GIS editor
  268. * @access public
  269. */
  270. public function generateWkt($gis_data, $index, $empty = '')
  271. {
  272. $no_of_lines = isset($gis_data[$index]['POLYGON']['no_of_lines'])
  273. ? $gis_data[$index]['POLYGON']['no_of_lines'] : 1;
  274. if ($no_of_lines < 1) {
  275. $no_of_lines = 1;
  276. }
  277. $wkt = 'POLYGON(';
  278. for ($i = 0; $i < $no_of_lines; $i++) {
  279. $no_of_points = isset($gis_data[$index]['POLYGON'][$i]['no_of_points'])
  280. ? $gis_data[$index]['POLYGON'][$i]['no_of_points'] : 4;
  281. if ($no_of_points < 4) {
  282. $no_of_points = 4;
  283. }
  284. $wkt .= '(';
  285. for ($j = 0; $j < $no_of_points; $j++) {
  286. $wkt .= ((isset($gis_data[$index]['POLYGON'][$i][$j]['x'])
  287. && trim($gis_data[$index]['POLYGON'][$i][$j]['x']) != '')
  288. ? $gis_data[$index]['POLYGON'][$i][$j]['x'] : $empty)
  289. . ' ' . ((isset($gis_data[$index]['POLYGON'][$i][$j]['y'])
  290. && trim($gis_data[$index]['POLYGON'][$i][$j]['y']) != '')
  291. ? $gis_data[$index]['POLYGON'][$i][$j]['y'] : $empty) . ',';
  292. }
  293. $wkt = substr($wkt, 0, strlen($wkt) - 1);
  294. $wkt .= '),';
  295. }
  296. $wkt = substr($wkt, 0, strlen($wkt) - 1);
  297. $wkt .= ')';
  298. return $wkt;
  299. }
  300. /**
  301. * Calculates the area of a closed simple polygon.
  302. *
  303. * @param array $ring array of points forming the ring
  304. *
  305. * @return float the area of a closed simple polygon
  306. * @access public
  307. * @static
  308. */
  309. public static function area($ring)
  310. {
  311. $no_of_points = count($ring);
  312. // If the last point is same as the first point ignore it
  313. $last = count($ring) - 1;
  314. if (($ring[0]['x'] == $ring[$last]['x'])
  315. && ($ring[0]['y'] == $ring[$last]['y'])
  316. ) {
  317. $no_of_points--;
  318. }
  319. // _n-1
  320. // A = _1_ \ (X(i) * Y(i+1)) - (Y(i) * X(i+1))
  321. // 2 /__
  322. // i=0
  323. $area = 0;
  324. for ($i = 0; $i < $no_of_points; $i++) {
  325. $j = ($i + 1) % $no_of_points;
  326. $area += $ring[$i]['x'] * $ring[$j]['y'];
  327. $area -= $ring[$i]['y'] * $ring[$j]['x'];
  328. }
  329. $area /= 2.0;
  330. return $area;
  331. }
  332. /**
  333. * Determines whether a set of points represents an outer ring.
  334. * If points are in clockwise orientation then, they form an outer ring.
  335. *
  336. * @param array $ring array of points forming the ring
  337. *
  338. * @return bool whether a set of points represents an outer ring
  339. * @access public
  340. * @static
  341. */
  342. public static function isOuterRing($ring)
  343. {
  344. // If area is negative then it's in clockwise orientation,
  345. // i.e. it's an outer ring
  346. if (PMA_GIS_Polygon::area($ring) < 0) {
  347. return true;
  348. }
  349. return false;
  350. }
  351. /**
  352. * Determines whether a given point is inside a given polygon.
  353. *
  354. * @param array $point x, y coordinates of the point
  355. * @param array $polygon array of points forming the ring
  356. *
  357. * @return bool whether a given point is inside a given polygon
  358. * @access public
  359. * @static
  360. */
  361. public static function isPointInsidePolygon($point, $polygon)
  362. {
  363. // If first point is repeated at the end remove it
  364. $last = count($polygon) - 1;
  365. if (($polygon[0]['x'] == $polygon[$last]['x'])
  366. && ($polygon[0]['y'] == $polygon[$last]['y'])
  367. ) {
  368. $polygon = array_slice($polygon, 0, $last);
  369. }
  370. $no_of_points = count($polygon);
  371. $counter = 0;
  372. // Use ray casting algorithm
  373. $p1 = $polygon[0];
  374. for ($i = 1; $i <= $no_of_points; $i++) {
  375. $p2 = $polygon[$i % $no_of_points];
  376. if ($point['y'] <= min(array($p1['y'], $p2['y']))) {
  377. $p1 = $p2;
  378. continue;
  379. }
  380. if ($point['y'] > max(array($p1['y'], $p2['y']))) {
  381. $p1 = $p2;
  382. continue;
  383. }
  384. if ($point['x'] > max(array($p1['x'], $p2['x']))) {
  385. $p1 = $p2;
  386. continue;
  387. }
  388. if ($p1['y'] != $p2['y']) {
  389. $xinters = ($point['y'] - $p1['y'])
  390. * ($p2['x'] - $p1['x'])
  391. / ($p2['y'] - $p1['y']) + $p1['x'];
  392. if ($p1['x'] == $p2['x'] || $point['x'] <= $xinters) {
  393. $counter++;
  394. }
  395. }
  396. $p1 = $p2;
  397. }
  398. if ($counter % 2 == 0) {
  399. return false;
  400. } else {
  401. return true;
  402. }
  403. }
  404. /**
  405. * Returns a point that is guaranteed to be on the surface of the ring.
  406. * (for simple closed rings)
  407. *
  408. * @param array $ring array of points forming the ring
  409. *
  410. * @return array|void a point on the surface of the ring
  411. * @access public
  412. * @static
  413. */
  414. public static function getPointOnSurface($ring)
  415. {
  416. // Find two consecutive distinct points.
  417. for ($i = 0, $nb = count($ring) - 1; $i < $nb; $i++) {
  418. if ($ring[$i]['y'] != $ring[$i + 1]['y']) {
  419. $x0 = $ring[$i]['x'];
  420. $x1 = $ring[$i + 1]['x'];
  421. $y0 = $ring[$i]['y'];
  422. $y1 = $ring[$i + 1]['y'];
  423. break;
  424. }
  425. }
  426. if (! isset($x0)) {
  427. return false;
  428. }
  429. // Find the mid point
  430. $x2 = ($x0 + $x1) / 2;
  431. $y2 = ($y0 + $y1) / 2;
  432. // Always keep $epsilon < 1 to go with the reduction logic down here
  433. $epsilon = 0.1;
  434. $denominator = sqrt(
  435. PMA_Util::pow(($y1 - $y0), 2)
  436. + PMA_Util::pow(($x0 - $x1), 2)
  437. );
  438. $pointA = array(); $pointB = array();
  439. while (true) {
  440. // Get the points on either sides of the line
  441. // with a distance of epsilon to the mid point
  442. $pointA['x'] = $x2 + ($epsilon * ($y1 - $y0)) / $denominator;
  443. $pointA['y'] = $y2 + ($pointA['x'] - $x2) * ($x0 - $x1) / ($y1 - $y0);
  444. $pointB['x'] = $x2 + ($epsilon * ($y1 - $y0)) / (0 - $denominator);
  445. $pointB['y'] = $y2 + ($pointB['x'] - $x2) * ($x0 - $x1) / ($y1 - $y0);
  446. // One of the points should be inside the polygon,
  447. // unless epcilon chosen is too large
  448. if (PMA_GIS_Polygon::isPointInsidePolygon($pointA, $ring)) {
  449. return $pointA;
  450. } elseif (PMA_GIS_Polygon::isPointInsidePolygon($pointB, $ring)) {
  451. return $pointB;
  452. } else {
  453. //If both are outside the polygon reduce the epsilon and
  454. //recalculate the points(reduce exponentially for faster convergance)
  455. $epsilon = PMA_Util::pow($epsilon, 2);
  456. if ($epsilon == 0) {
  457. return false;
  458. }
  459. }
  460. }
  461. }
  462. /** Generate parameters for the GIS data editor from the value of the GIS column.
  463. *
  464. * @param string $value Value of the GIS column
  465. * @param int $index Index of the geometry
  466. *
  467. * @return array params for the GIS data editor from the value of the GIS column
  468. * @access public
  469. */
  470. public function generateParams($value, $index = -1)
  471. {
  472. $params = array();
  473. if ($index == -1) {
  474. $index = 0;
  475. $data = PMA_GIS_Geometry::generateParams($value);
  476. $params['srid'] = $data['srid'];
  477. $wkt = $data['wkt'];
  478. } else {
  479. $params[$index]['gis_type'] = 'POLYGON';
  480. $wkt = $value;
  481. }
  482. // Trim to remove leading 'POLYGON((' and trailing '))'
  483. $polygon = substr($wkt, 9, (strlen($wkt) - 11));
  484. // Seperate each linestring
  485. $linerings = explode("),(", $polygon);
  486. $params[$index]['POLYGON']['no_of_lines'] = count($linerings);
  487. $j = 0;
  488. foreach ($linerings as $linering) {
  489. $points_arr = $this->extractPoints($linering, null);
  490. $no_of_points = count($points_arr);
  491. $params[$index]['POLYGON'][$j]['no_of_points'] = $no_of_points;
  492. for ($i = 0; $i < $no_of_points; $i++) {
  493. $params[$index]['POLYGON'][$j][$i]['x'] = $points_arr[$i][0];
  494. $params[$index]['POLYGON'][$j][$i]['y'] = $points_arr[$i][1];
  495. }
  496. $j++;
  497. }
  498. return $params;
  499. }
  500. }
  501. ?>