【JavaScirpt】【Leaflet】任意の多角形領域の外側だけ色付けしたレイヤーで覆う方法とその内外判定

 Leafletは地図を表現するためのJavaScriptのライブラリです。これはよく次のコード(デモの各タブから参照してください)の様にタイルレイヤーとして様々な世界地図を読み込ませることで動かせます。
Leaflet

 時たま、地図中のある領域のみを扱いたいという要望があります。これを満たしている状態を視覚的に示すためには、その領域のみ通常の地図通り表示して、領域の外部を色付きにするのが良いです。これは次でできます。

 素の地図表示から追加されたコードは以下です。


// 2点から四角になる様に4点の座標配列を生成
const makeBox = (x1, y1, x2, y2) => {
  return [
    [x1, y1],
    [x1, y2],
    [x2, y2],
    [x2, y1],
  ];
};
// 領域のセット
const HAMAMATSU_LU_LAT = 34.68;
const HAMAMATSU_LU_LNG = 137.65;
const HAMAMATSU_RD_LAT = 34.75;
const HAMAMATSU_RD_LNG = 137.73;
// 領域の外側を定義
const polygonLatLngList = [
  makeBox(-90, -180, 90, 180),// 世界全土を外枠
  makeBox(HAMAMATSU_LU_LAT, HAMAMATSU_LU_LNG, HAMAMATSU_RD_LAT, HAMAMATSU_RD_LNG),// 浜松の一部を内枠
];
// Polygon で領域外部を覆うドーナツ的なレイヤーを描画
L.polygon(polygonLatLngList, {
  color: 'black',
  opacity: 0, // 境界線の透明度。0 にすることで非表示化
  fillOpacity: 0.6,// ドーナツの身の部分の透明度
}).addTo(map);

 ここで使っているのは Polygon で、これを使うと非常に自由な多角形を定義できます。
Documentation – Leaflet – a JavaScript library for interactive maps#polygon
 デモの様に [[number, number][], [number, number][]] な配列を用意することで穴の空いた多角形も定義できます。これの表示部の内部にのみマーカーを置ける様にします。これは次でできます。

 追加したコードは次です。

let marker;// 常に一つであるマーカーを持っておく変数
// マップクリック時のイベント関数
const onMapClick = (e) => {
  // 内外判定の結果を変数に格納
  const isOutsideRect =
    e.latlng.lat <= HAMAMATSU_LU_LAT  || HAMAMATSU_RD_LAT <= e.latlng.lat
    || e.latlng.lng  <= HAMAMATSU_LU_LNG || HAMAMATSU_RD_LNG <= e.latlng.lng;
  if (isOutsideRect) {
    // 外なら処理なし
    return;
  }
  if (marker) {
    // 既存のマーカーがあれば削除して後述の追加によって入れ替え
    marker.remove(); 
  }
  // マーカーを地図上に追加
  marker = L.marker(e.latlng)
      .addTo(map)
      .bindPopup(e.latlng.toString())
      .openPopup();
};
// click イベントを追加
map.on('click', onMapClick);

 クリックした時 Leaflet から渡される緯度経度が箱の外にあるか内にあるかを判定して、あるならばマーカーを地図上にポップアップ付き状態でセットしています。内外判定は最北端より北の緯度を持つ or 最西端より……といった比較的シンプルな判定手法です。
Documentation – Leaflet – a JavaScript library for interactive maps#marker”
Documentation – Leaflet – a JavaScript library for interactive maps#map-click
 今までのデモでは四角形を取り扱いましたが Leaflet の Polygon では字義通りの意味の多角形を取り扱えます。多角形を描画し、内部のみクリックでマーカーを置けるようにしたデモが次です。

 多角形の描画コードが次です。

// 2点から四角になる様に4点の座標配列を生成
const makeBox = (x1, y1, x2, y2) => {
  return [
    [x1, y1],
    [x1, y2],
    [x2, y2],
    [x2, y1],
  ];
};
// 領域セット
const polygonInnerLatLngList = [
  [34.68, 137.73],
  [34.7, 137.72],
  [34.73, 137.71],
  [34.73, 137.70],
  [34.72, 137.69],
  [34.7, 137.69],
  [34.68, 137.64],
  [34.65, 137.65],
  [34.64, 137.65],
  [34.69, 137.67],
  [34.68, 137.68],
];
const polygonLatLngList = [makeBox(-90, -180, 90, 180), polygonInnerLatLngList];

const pol = L.polygon(polygonLatLngList, {
  color: 'black',
  opacity: 0, 
  fillOpacity: 0.6,
}).addTo(map);

 ここは四角の場合とほとんど変わりません。元々 4 点を指定していた内側の領域の点が多数になっているのみです。四角形同然のコードで多角形を Leaflet 上で表現できます。
 マーカーを置くコードはほぼ同じで、内外判定のみが変わります。ここで用いたのは
【第2回】点の多角形に対する内外判定|【技業LOG】技術者が紹介するNTTPCのテクノロジー|【公式】NTTPC
Inclusion of a Point in a Polygon
Point in polygon – Wikipedia
 を大いに参考にした Crossing Number Algorithm(Ray casting algorithm とも。ググった感じこちらの名前の方がメジャーの様です)です。簡易に取り扱うために配列を引数とした一関数にしました。

/**
 * 点が多角形の内にあるならば true
 * @param {[Number, Number]} point
 * @param {[Number, Number][]} polygon
 * @return {boolean}
 */
function isInsideByCrossingNumberAlgorithm(point, polygon) {
  const x = point[0];
  const y = point[1];

  let crossCount = 0;
  for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
    // 多角形を成す辺を全て調査する
    // 現在の index の多角形の点X, Y
    const polCX = polygon[i][0];
    const polCY = polygon[i][1];
    // 次の index の多角形の点X, Y
    const polNX = polygon[j][0];
    const polNY = polygon[j][1];

    if (polCY > y === polNY > y) {
      // 点が辺の完全に上 or 完全に下ならば. ノーカウントでループ続行
      continue;
    }

    // 辺が点pと同じ高さになる位置を特定し、その時のxの値と点pのxの値を比較する
    // 同じ高さになる時の辺の割合 = (点Y - 始点Y座標)) / 辺のY軸長さ
    const vt = (y - polCY) / (polNY - polCY);
    // 点のX座標 < 辺のX軸長さ * 同じ高さになる時の辺の割合 + 始点X座標
    if (x < (polNX - polCX) * vt + polCX) {
      crossCount++;
    }
  }

  return crossCount % 2 === 1;
}

 これで任意の多角形領域の外側だけ色付けしたレイヤーで覆い、クリックされた部分が領域の内外かわかるようになります(この記事ではクリックされた部分が内側ならばマーカーを立てる、としました)。

追記: スクラッチするよりも相当人数がデバッグ済みであろう次のライブラリの方が安心安全なので、それを使った方が良いです。
substack/point-in-polygon: determine if a point is inside a polygon
point-in-polygon – npm

>株式会社シーポイントラボ

株式会社シーポイントラボ

TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:〒432-8003
   静岡県浜松市中央区和地山3-1-7
   浜松イノベーションキューブ 315
※ご来社の際はインターホンで「316」をお呼びください

CTR IMG