在PHP中查找多边形内的点

31

我有一个关于MySQL的几何数据类型多边形(polygon)的典型问题。

我的多边形数据以纬度和经度数组的形式存在,例如:

[["x":37.628134,  "y":-77.458334],
["x":37.629867,   "y":-77.449021],
["x":37.62324,    "y":-77.445416],
["x":37.622424,   "y":-77.457819]]

我有一个具有纬度和经度坐标的点(顶点),例如:

$location = new vertex($_GET["longitude"], $_GET["latitude"]);

现在我想找出这个顶点(点)是否在多边形内部。 我该如何在php中实现?


你的多边形保证是凸多边形吗? - awm
哦,很酷,你在做什么? - user479911
我不知道它是凸的还是凹的,基本上我正在用一组顶点形成一个多边形,这些顶点代表特定地理位置的纬度和经度。我想找出一个几何点(顶点)是否在多边形内部。 - shasi kanth
1
在https://dev59.com/p3VC5IYBdhLWcg3wrDJd的答案中有一个非常好的解释,其中包含可以轻松移植到PHP的代码。 - Mark Baker
8个回答

58

这是我从另一种语言转换为 PHP 的一个函数:

$vertices_x = array(37.628134, 37.629867, 37.62324, 37.622424);    // x-coordinates of the vertices of the polygon
$vertices_y = array(-77.458334,-77.449021,-77.445416,-77.457819); // y-coordinates of the vertices of the polygon
$points_polygon = count($vertices_x) - 1;  // number vertices - zero-based array
$longitude_x = $_GET["longitude"];  // x-coordinate of the point to test
$latitude_y = $_GET["latitude"];    // y-coordinate of the point to test

if (is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)){
  echo "Is in polygon!";
}
else echo "Is not in polygon";


function is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)
{
  $i = $j = $c = 0;
  for ($i = 0, $j = $points_polygon ; $i < $points_polygon; $j = $i++) {
    if ( (($vertices_y[$i]  >  $latitude_y != ($vertices_y[$j] > $latitude_y)) &&
     ($longitude_x < ($vertices_x[$j] - $vertices_x[$i]) * ($latitude_y - $vertices_y[$i]) / ($vertices_y[$j] - $vertices_y[$i]) + $vertices_x[$i]) ) )
       $c = !$c;
  }
  return $c;
}

补充: 如果你需要更多的功能,我建议你使用polygon.php类,可以在这里找到。 使用你的顶点创建这个类,并将你的测试点作为输入调用isInside函数,以解决你的问题。


4
+1 - 请访问http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html了解其工作原理的说明。 - Mark Baker
7
这里还发现了另一个可行的示例:http://www.assemblysys.com/dataServices/php_pointinpolygon.php。 - shasi kanth
12
这个算法对于多边形的X和Y都是正数的情况非常适用,但由于问题涉及经度和纬度,是否只有我认为,如果多边形被本初子午线穿过,即一个点的经度为正,如1.000000,而下一个点则为负,如-1.000000,那么这个算法将彻底失败? 解决方案:将所有经度偏移+180(这不是向东移动到中国,而是使所有经度为正 :-)) - Ognyan
这个算法是否假定多边形不是自闭合的?也就是说,它的最后一个顶点是倒数第二个点和最后一个点之间的线段,而不是最后一个点和第一个点之间的线段,这是自闭合多边形的情况。_注意:我正在使用它来处理SVG文件_。 - Martin Joiner
1
@Ogre_BGR 是正确的,我已经在这里发布了一个可靠的证明版本 -> https://dev59.com/GmUq5IYBdhLWcg3wEcVZ#18190354 - davidkonrad
显示剩余5条评论

15

上面流行的答案存在拼写错误。在其他地方,这段代码已经被整理过了。更正后的代码如下:

<?php
/**
  From: http://www.daniweb.com/web-development/php/threads/366489
  Also see http://en.wikipedia.org/wiki/Point_in_polygon
*/
$vertices_x = array(37.628134, 37.629867, 37.62324, 37.622424); // x-coordinates of the vertices of the polygon
$vertices_y = array(-77.458334,-77.449021,-77.445416,-77.457819); // y-coordinates of the vertices of the polygon
$points_polygon = count($vertices_x); // number vertices
$longitude_x = $_GET["longitude"]; // x-coordinate of the point to test
$latitude_y = $_GET["latitude"]; // y-coordinate of the point to test
//// For testing.  This point lies inside the test polygon.
// $longitude_x = 37.62850;
// $latitude_y = -77.4499;

if (is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)){
  echo "Is in polygon!";
}
else echo "Is not in polygon";


function is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)
{
  $i = $j = $c = 0;
  for ($i = 0, $j = $points_polygon-1 ; $i < $points_polygon; $j = $i++) {
    if ( (($vertices_y[$i] > $latitude_y != ($vertices_y[$j] > $latitude_y)) &&
    ($longitude_x < ($vertices_x[$j] - $vertices_x[$i]) * ($latitude_y - $vertices_y[$i]) / ($vertices_y[$j] - $vertices_y[$i]) + $vertices_x[$i]) ) ) 
        $c = !$c;
  }
  return $c;
}
?>

这个函数运行得很好,但如果测试点等于其中一个顶点,则无法正常工作。这是一个简单的测试用例。此外,您必须注意您的多边形不要跨越国际日期变更线。如果需要这样做,您必须将多边形分解为两个位于日期变更线两侧的多边形。 - Jake
你纠正了哪些具体的打字错误?就我所看到的,你所做的只是将“-1”从“is_in_polygon()”函数外部移动到内联。 - Martin Joiner
最初提供的代码无法正确解析。自我的回答之后,似乎已经修复了。请参见此处:http://stackoverflow.com/posts/5065219/revisions - amh15

7
上述解决方案不符合我的预期,相反,您可以选择以下解决方案。
  1. With PHP

    function pointInPolygon($point, $polygon, $pointOnVertex = true) {
        $this->pointOnVertex = $pointOnVertex;
    
        // Transform string coordinates into arrays with x and y values
        $point = $this->pointStringToCoordinates($point);
        $vertices = array(); 
        foreach ($polygon as $vertex) {
            $vertices[] = $this->pointStringToCoordinates($vertex); 
        }
    
        // Check if the lat lng sits exactly on a vertex
        if ($this->pointOnVertex == true and $this->pointOnVertex($point, $vertices) == true) {
            return "vertex";
        }
    
        // Check if the lat lng is inside the polygon or on the boundary
        $intersections = 0; 
        $vertices_count = count($vertices);
    
        for ($i=1; $i < $vertices_count; $i++) {
            $vertex1 = $vertices[$i-1]; 
            $vertex2 = $vertices[$i];
            if ($vertex1['y'] == $vertex2['y'] and $vertex1['y'] == $point['y'] and $point['x'] > min($vertex1['x'], $vertex2['x']) and $point['x'] < max($vertex1['x'], $vertex2['x'])) { // Check if point is on an horizontal polygon boundary
                return "boundary";
            }
            if ($point['y'] > min($vertex1['y'], $vertex2['y']) and $point['y'] <= max($vertex1['y'], $vertex2['y']) and $point['x'] <= max($vertex1['x'], $vertex2['x']) and $vertex1['y'] != $vertex2['y']) { 
                $xinters = ($point['y'] - $vertex1['y']) * ($vertex2['x'] - $vertex1['x']) / ($vertex2['y'] - $vertex1['y']) + $vertex1['x']; 
                if ($xinters == $point['x']) { // Check if lat lng is on the polygon boundary (other than horizontal)
                    return "boundary";
                }
                if ($vertex1['x'] == $vertex2['x'] || $point['x'] <= $xinters) {
                    $intersections++; 
                }
            } 
        } 
        // If the number of edges we passed through is odd, then it's in the polygon. 
        if ($intersections % 2 != 0) {
            return "inside";
        } else {
            return "outside";
        }
    }
    
    function pointOnVertex($point, $vertices) {
      foreach($vertices as $vertex) {
          if ($point == $vertex) {
              return true;
          }
      }
    
    }
    
    function pointStringToCoordinates($pointString) {
        $coordinates = explode(" ", $pointString);
        return array("x" => $coordinates[0], "y" => $coordinates[1]);
    }
    // Function to check lat lng
    function check(){
        $points = array("22.367582 70.711816", "21.43567582 72.5811816","22.367582117085913 70.71181669186944","22.275334996986643 70.88614147123701","22.36934302329968 70.77627818998701"); // Array of latlng which you want to find
        $polygon = array(
            "22.367582117085913 70.71181669186944",
            "22.225161442616514 70.65582486840117",
            "22.20736264867434 70.83229276390898",
            "22.18701840565626 70.9867880031668",
            "22.22452581029355 71.0918447658621",
            "22.382709129816103 70.98884793969023",
            "22.40112042636022 70.94078275414336",
            "22.411912121843205 70.7849142238699",
            "22.367582117085913 70.71181669186944"
        );
        // The last lat lng must be the same as the first one's, to "close the loop"
        foreach($points as $key => $point) {
            echo "(Lat Lng) " . ($key+1) . " ($point): " . $this->pointInPolygon($point, $polygon) . "<br>";
        }
    }
    
  2. With MySql

CREATE TABLE `TestPoly` (
   `id` int(11) NOT NULL,
   `name` varchar(255) NOT NULL,
   `pol` polygon NOT NULL
 )

SET @g = 'POLYGON((22.367582117085913 70.71181669186944, 22.225161442616514 70.65582486840117, 22.20736264867434 70.83229276390898, 22.18701840565626 70.9867880031668, 22.22452581029355 71.0918447658621, 22.382709129816103 70.98884793969023, 22.40112042636022 70.94078275414336, 22.411912121843205 70.7849142238699, 22.367582117085913 70.71181669186944))';
INSERT INTO TestPoly (pol) VALUES (ST_GeomFromText(@g))

set @p = GeomFromText('POINT(22.4053386588057 70.86240663480157)');
select * FROM TestPoly where ST_Contains(pol, @p);

请问一下哪个解决方案出了问题? - Renish Gotecha
以下是程序相关内容的翻译。请仅返回已翻译的文本:这里是详情:https://stackoverflow.com/questions/61302366/point-in-polygon-algorithm-giving-wrong-results-for-negative-points - user3997016
你可以使用你的输入检查下面的URL。我同意谷歌地图显示了正确的多边形。为了解决问题,你必须使用我提供的MySQL解决方案。它完全可以正常工作。 https://www.keene.edu/campus/maps/tool/ - Renish Gotecha
我也尝试了MySql的解决方案,但它并没有起作用。你也可以从你的端口进行检查。 - user3997016
这段代码来自 https://assemblysys.com/php-point-in-polygon-algorithm/。你应该添加署名。 - cootje

5
如果你的多边形是自我闭合的,也就是说它的最后一个顶点是最后一个点和第一个点之间的线段,那么你需要在循环中添加一个变量和一个条件来处理最后一个顶点。同时,你还需要将顶点数传递为点数相等的值。
以下是修改后的被接受的答案,可用于处理自我闭合的多边形:
$vertices_x = array(37.628134, 37.629867, 37.62324, 37.622424);    // x-coordinates of the vertices of the polygon
$vertices_y = array(-77.458334,-77.449021,-77.445416,-77.457819); // y-coordinates of the vertices of the polygon
$points_polygon = count($vertices_x);  // number vertices = number of points in a self-closing polygon
$longitude_x = $_GET["longitude"];  // x-coordinate of the point to test
$latitude_y = $_GET["latitude"];    // y-coordinate of the point to test

if (is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)){
  echo "Is in polygon!";
}
else echo "Is not in polygon";


function is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)
{
  $i = $j = $c = $point = 0;
  for ($i = 0, $j = $points_polygon ; $i < $points_polygon; $j = $i++) {
    $point = $i;
    if( $point == $points_polygon )
      $point = 0;
    if ( (($vertices_y[$point]  >  $latitude_y != ($vertices_y[$j] > $latitude_y)) &&
     ($longitude_x < ($vertices_x[$j] - $vertices_x[$point]) * ($latitude_y - $vertices_y[$point]) / ($vertices_y[$j] - $vertices_y[$point]) + $vertices_x[$point]) ) )
       $c = !$c;
  }
  return $c;
}

感谢您!我发现这个页面和它的被接受的答案非常有帮助,我很自豪地提供这个变体。

你好,请检查一下这个链接 - https://stackoverflow.com/questions/61302366/point-in-polygon-algorithm-giving-wrong-results-for-negative-points - user3997016

5
我将泰国多边形数据存入了MySQL,并将已接受的答案功能与MySQL 8内置功能进行了比较。
CREATE TABLE `polygons` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `polygon` POLYGON NOT NULL,
    `country` VARCHAR(50) NULL DEFAULT NULL,
    PRIMARY KEY (`id`),
    SPATIAL INDEX `polygon` (`polygon`)
)
COLLATE='utf8mb4_0900_ai_ci'
ENGINE=InnoDB
AUTO_INCREMENT=652
;

INSERT INTO `polygons` (`country`, `polygon`) VALUES ('Thailand', ST_GEOMFROMTEXT('POLYGON((102.1728516 6.1842462,101.6894531 5.7253114,101.1401367 5.6815837,101.1181641 6.2497765,100.1074219 6.4899833,96.3281250 6.4244835,96.1083984 9.8822755,98.7670898 10.1419317,99.5800781 11.8243415,98.2177734 15.1569737,98.9868164 16.3201395,97.4267578 18.4587681,98.1079102 19.7253422,99.0087891 19.7460242,100.2612305 20.2828087,100.4809570 19.4769502,101.2060547 19.4147924,100.8544922 17.4135461,102.0849609 17.9996316,102.8320313 17.7696122,103.3593750 18.3545255,104.7875977 17.4554726,104.6337891 16.4676947,105.5126953 15.6018749,105.2270508 14.3069695,102.9858398 14.2643831,102.3486328 13.5819209,103.0297852 11.0059045,103.6669922 8.5592939,102.1728516 6.1842462))'));

这是一个带有点的多边形 - 红色 是第一个,蓝色 是最后一个。

Thailand Polygon Dots

我使用 https://www.gpsvisualizer.com/draw/ 在泰国多边形地图上绘制了一些内外的点,并创建了屏幕以可视化所有的点。

Dots difference MySQL / PHP

我将点作为PHP函数的坐标,并使用查询比较结果与MySQL函数:

SELECT TRUE FROM `polygons` WHERE `polygons`.`country` = 'Thailand' AND ST_CONTAINS(`polygons`.`polygon`, POINT($long, $lat));

结果:

  • MySQL总是能正确地给出所有点的答案。
  • PHP函数有错误的答案:
    • 红色 - 如果我删除多边形的结束点
    • 橙色 - 不删除与开头相同的最后一个点,与MYSQL多边形相同。
    • 白色点在PHP / MySQL中具有相同的结果并且是正确的答案

我尝试改变多边形,但php函数总是会犯错,意味着我找不到其中的错误。

更新1

找到解决方案assemblysys.com/php-point-in-polygon-algorithm - 此算法与MySQL算法相同!

更新2

比较了PHP速度和MySQL(我认为PHP应该更快),但没有。比较了47k个点。

18-06-2020 21:34:45 - PHP Speed Check Start
18-06-2020 21:34:51 - FIN! PHP Check. NOT = 41085 / IN = 5512
18-06-2020 21:34:51 - MYSQL Speed Check Start
18-06-2020 21:34:58 - FIN! MYSQL Check. NOT = 41085 / IN = 5512

创立的解决方案 https://assemblysys.com/php-point-in-polygon-algorithm/ - 这个算法与Mysql算法相同! - wtfowned

3
这里提供一个可能的算法。
  1. 建立以你感兴趣的点为中心的新坐标系。
  2. 在新的坐标系中,将多边形的所有顶点转换为极坐标。
  3. 遍历多边形,记录角度变化的总和 ∆θ。始终使用每个角度变化的最小值。
  4. 如果当你遍历整个多边形后,总的 ∆θ 等于 0,那么你在多边形外部。另一方面,如果它是 ±2π,则你在内部。
  5. 如果 ∆θ > 2π 或 ∆θ <-2π,那就意味着你有一个自相交的多边形。
编写代码留给读者自行探索。:)

抱歉,我无法理解这个场景...它看起来非常复杂。有没有示例代码或链接? - shasi kanth
1
可能有一个复杂数学函数库存在某个地方。也许其他人知道它在哪里(我不知道)。如果你打算自己编写代码,那么我的答案才有用。 :) - awm

3

更新了代码以便更容易与Google Maps一起使用: 它接受类似于以下的数组:

Array
(
    [0] => stdClass Object
        (
            [lat] => 43.685927
            [lng] => -79.745829
        )

    [1] => stdClass Object
        (
            [lat] => 43.686004
            [lng] => -79.745954
        )

    [2] => stdClass Object
        (
            [lat] => 43.686429
            [lng] => -79.746642
        )

这样使用谷歌地图会更加容易:

function is_in_polygon2($longitude_x, $latitude_y,$polygon)
{
  $i = $j = $c = 0;
  $points_polygon = count($polygon)-1;
  for ($i = 0, $j = $points_polygon ; $i < $points_polygon; $j = $i++) {
    if ( (($polygon[$i]->lat  >  $latitude_y != ($polygon[$j]->lat > $latitude_y)) &&
     ($longitude_x < ($polygon[$j]->lng - $polygon[$i]->lng) * ($latitude_y - $polygon[$i]->lat) / ($polygon[$j]->lat - $polygon[$i]->lat) + $polygon[$i]->lng) ) )
       $c = !$c;
  }
  return $c;
}

2

我在php codeigniter中编写了代码,在我的控制器中创建了两个函数,如下所示:

Original Answer翻译成"最初的回答"

public function checkLatLng(){
    $vertices_y = array(22.774,22.174,22.466,22.666,22.966,22.321);    // x-coordinates of the vertices of the polygon (LATITUDES)
    $vertices_x = array(70.190,70.090,77.118,77.618,77.418,77.757); // y-coordinates of the vertices of the polygon (LONGITUDES)
    $points_polygon = count($vertices_x)-1; 
    $longitude_x = $this->input->get("longitude");  // Your Longitude
    $latitude_y = $this->input->get("latitude");    // Your Latitude
    if ($this->is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y)){
        echo "Is in polygon!";
    }
    else
        echo "Is not in polygon";
}

另一个检查经纬度的函数如下:

最初的回答

public function is_in_polygon($points_polygon, $vertices_x, $vertices_y, $longitude_x, $latitude_y){
    $i = $j = $c = $point = 0;
    for ($i = 0, $j = $points_polygon ; $i < $points_polygon; $j = $i++) {
        $point = $i;
        if( $point == $points_polygon )
            $point = 0;
        if ( (($vertices_y[$point]  >  $latitude_y != ($vertices_y[$j] > $latitude_y)) && ($longitude_x < ($vertices_x[$j] - $vertices_x[$point]) * ($latitude_y - $vertices_y[$point]) / ($vertices_y[$j] - $vertices_y[$point]) + $vertices_x[$point]) ) )
            $c = !$c;
    }
    return $c;
}

为了您的测试目的,我传递了以下内容:

纬度=22.808059

经度=77.522014

我的多边形:

enter image description here

最初的回答。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接