ブログに書くつもりじゃなかった

フリーのプログラマーが綴る、裏チラ系の備忘録や雑記帳。

Java + Spatial4jでGeofence

やりたいこと

多角形(ポリゴン)または円(サークル)でGeofenceを定義する。それでデバイスの現在位置がGeofenceの内側にいるか外側にいるかを判定する。

こんなイメージ。 f:id:edts:20210109133554p:plain   © OpenStreetMap contributors

Spatial4jとは

地理空間情報を扱うためのJavaライブラリ。公式サイトはこちら

試してみる

pom.xml
<dependency>
    <groupId>org.locationtech.spatial4j</groupId>
    <artifactId>spatial4j</artifactId>
    <version>0.8</version>
</dependency>
多角形(ポリゴン)型Geofence
import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.io.WKTReader;
import org.locationtech.spatial4j.shape.Shape;

public class Spatial4jPolygonTest {

    public static void main(String[] args) {

        JtsSpatialContext ctx = JtsSpatialContext.GEO;  // (1)

        try {
                WKTReader reader = new WKTReader(ctx, null); // (2)
            // 仙台市営地下鉄南北線、東西線のターミナルを頂点としたPolygonをGeoFenceとする
            Shape polygon = reader.read("POLYGON(("
                    + "140.880560 38.324137, "  // 泉中央駅
                    + "140.948398 38.244902, "  // 荒井駅
                    + "140.870713 38.214264, "  // 富沢駅
                    + "140.843720 38.243071, "  // 八木山動物公園駅
                    + "140.880560 38.324137"    // 泉中央駅
                    + "))"); // (3)

            // デバイスが仙台駅にあるケース
            Shape point0 = reader.read("POINT(140.882448 38.260300)");   // (4)
            System.out.println(polygon.relate(point0)); // (5)

            // デバイスが小鶴新田駅にあるケース
            Shape point1 = reader.read("POINT(140.934837 38.273621)");
            System.out.println(polygon.relate(point1));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 何をやるにしてもJtsSpatialContextが必要になるので、まずはこれを取得する。JtsSpatialContext.GEOでシングルトンインスタンスが用意されているので、そのまま使用する。
  2. 今回はGeofenceと位置情報をWKT(Well-known text)で用意する。そのため、WKTReaderインスタンスを作成する。WKTの詳しい説明はウィキペディアが参考になる。
  3. GeofenceをPolygonで表したWKTWKTReaderに読み込ませて、Shapeオブジェクトを作成する。
  4. 比較対象とするデバイスの位置情報はPointで表す。Geofenceと同様にShapeオブジェクトを作成する。
  5. GeofenceのShape.relate()を呼び出し、2つのオブジェクトの関係を求める。

実行するとこうなる。

CONTAINS
DISJOINT

Shape.relate()の戻り値はSpatialRelationEnum。PolygonとPointとの比較だとCONTAINS(比較対象が含まれている)、 DISJOINT(比較対象との接点がない)のどちらかしか返さないが、他にINTERSECTS(比較対象と交差している)、 WITHIN(比較対象の中にある)がある。

ちなみに、180度経線(下の図の赤い線)を跨いで領域を定義した場合、ライブラリによってはうまく結果を返さないものもある。Spatial4jは問題なく動いた。これならフィジー諸島とかロシアでもバッチリ監視できる。

f:id:edts:20210109135109p:plain © OpenStreetMap contributors

円(サークル)型Geofence

Geofenceの中心とデバイスの位置情報の2点間の距離を測れば事足りる気がする。だけど、円を表すGeoCircleクラスがあるので、せっかくなので使ってみる。

import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.io.WKTReader;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.impl.GeoCircle;

public class Spatial4jCircleTest {

    public static void main(String[] args) {

        JtsSpatialContext ctx = JtsSpatialContext.GEO;

        try {
        WKTReader reader = new WKTReader(ctx, null);

            Shape center = reader.read("POINT(140.878100 38.288440)"); // 台原駅
            // 半径2kmをdegreeに変換
            double radiusDEG = DistanceUtils.dist2Degrees(2.0d, DistanceUtils.EARTH_MEAN_RADIUS_KM);  // (1)
            // 台原駅を中心とした半径2kmをGeofenceとする
            GeoCircle circle = new GeoCircle((Point) center, radiusDEG, ctx); // (2)

     // デバイスが旭ヶ丘駅にあるケース
            Shape point0 = reader.read("POINT(140.885062 38.296344)");
            System.out.println(circle.relate(point0));

     // デバイスが八乙女駅にあるケース
            Shape point1 = reader.read("POINT(140.883689 38.312862)");
            System.out.println(circle.relate(point1));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. GeoCircleの半径はdegree(度)で指定するので、「中心から半径2km」のように指定する場合は、DistanceUtils.dist2Degrees()で変換処理を行う必要がある。ちなみにDistanceUtilsEARTH_MEAN_RADIUS_KM(平均の半径?: 約6371km)とEARTH_EQUATORIAL_RADIUS_KM(赤道半径: 約6378km)と2つの半径がある。ここではGoogle Mapの距離測定と結果が近い方のEARTH_MEAN_RADIUS_KMを採用。
  2. 中心と半径、JtsSpatialContentを指定してGeofenceを表すGeoCircleオブジェクトを作成。

以降は先ほどと同様。実行するとこうなる。

CONTAINS
DISJOINT

最後に

Big Brother is watching you.