如何判斷一個點在地圖上?如何判斷一個點在多邊形內?

子龍_子龍發表於2023-09-30
highlight: a11y-dark

近期,有接手到一個echarts地圖圖表專案,因為採集的散點資料很多打不到準確的地圖點上,故有了這個問題。

一般而言,標題的兩個問題其是同一個問題,因為對與一個地圖資料,也就是geoJson來說,其實就是一個有很多個點的多邊形。

目前來說判斷點是否在一個多邊形內,江湖上流傳的主要方法有射線法,面積法,叉積(凸多邊形)等等很多方法。但就筆者看各種技術文章,以及多方探(bai)索(du)研究(google)的情況下來看,射線法是比較常用的一種,基於射線法又派生出奇偶規則(Odd-even Rule)非零環繞數規則(Nonzero Winding Number Rule)

全文可能就只會用到一個數學公式直線方程的兩點式:

\(\frac{y-y2}{y1-y2} = \frac{x-x2}{x1-x2}\)

射線法

基本思想: 所謂射線法,就是指,從被測點引出一條射線,而後判斷與多邊形的交點。與邊的交點的個數決定了當前點是否在多邊形內,這是奇偶規則與非零環繞數規則的共同點,兩者的不同點在於是否考慮被交邊的方向以及對點的交點個數的判斷。

奇偶規則(Odd-even Rule)

所謂奇偶規則是指,若交點數為奇數則點在多邊形內,否則點在多邊形外。

圖示:

image

程式碼示例

/**
 * 
 * @param {*} param0 [number,number]
 * @param {*} points [number,number][]
 * @returns 
 */
function oddEvenRule([x,y], points) {

  let ans = false;
  if (points.length < 3) {
    return ans;
  }

  for (let i = 0, L = points.length - 1; i < L; i++ ) {
    const point1 = points[i];
    const point2 = points[i+1]

    const [x1, y1] = point1;
    const [x2, y2] = point2;

    if (y < Math.min(y1, y2) || y > Math.max(y1, y2)) { // 限定 y 在  y1 及y2之間
      continue;
    }
    // 在 point1及point2確定的直線上,根據待測點的y,求出交點座標x
    // 直線方程。兩點式(y-y2)/(y1-y2) = (x-x2)/(x1-x2)
    let crossoverX = (y - y2) * (x1 - x2) /(y1 - y2) + x2;
    if (y1 === y2) { // 水平邊的交點即為待測點的座標 暫時先不管了 感興趣可以自己處理一下
      // crossoverX = x;
      continue;
    }
    if (crossoverX > x) { // 只考慮一個方向
      ans = !ans; 
    }

  }

  return ans;
    
}

判斷一般的多邊形,奇偶規則判斷就夠了,但是奇偶規則有個限制,就是一個多邊形有多部分構成的時候,考慮如下場景:
image

此時再使用奇偶規則去判斷就不能準確判斷了,由此引出下面一個方法:

非零環繞數規則(Nonzero Winding Number Rule)

所謂非零環繞,是指,基於射線法,當射線穿過多邊形邊的時候,根據多邊形邊的方向,給總的交點數加1或者減1,最後判斷總的節點數是不是0,不是0則為多邊形內部,否則就是多邊形外部。

圖示:

image

可以看到非零環繞規則很好的解決了上邊的問題

程式碼示例

這裡援引一下zRender的contain原始碼

var EPSILON = 1e-8;
function isAroundEqual(a, b) {
    return Math.abs(a - b) < EPSILON;
}
function contain(points, x, y) {
    var w = 0;
    var p = points[0];
    if (!p) {
        return false;
    }
    for (var i = 1; i < points.length; i++) {
        var p2 = points[i];
        w += windingLine(p[0], p[1], p2[0], p2[1], x, y);
        p = p2;
    }
    // Close polygon
    var p0 = points[0];
    if (!isAroundEqual(p[0], p0[0]) || !isAroundEqual(p[1], p0[1])) {
        w += windingLine(p[0], p[1], p0[0], p0[1], x, y);
    }
    return w !== 0;
}
function windingLine(x0, y0, x1, y1, x, y) {
    if ((y > y0 && y > y1) || (y < y0 && y < y1)) {
        return 0;
    }
    // Ignore horizontal line
    if (y1 === y0) {
        return 0;
    }
    var t = (y - y0) / (y1 - y0);
    var dir = y1 < y0 ? 1 : -1;
    // Avoid winding error when intersection point is the connect point of two line of polygon
    if (t === 1 || t === 0) {
        dir = y1 < y0 ? 0.5 : -0.5;
    }
    var x_ = t * (x1 - x0) + x0;
    // If (x, y) on the line, considered as "contain".
    return x_ === x ? Infinity : x_ > x ? dir : 0;
}

相關文章