Java + Spatial4jでGeofence
やりたいこと
多角形(ポリゴン)または円(サークル)でGeofenceを定義する。それでデバイスの現在位置がGeofenceの内側にいるか外側にいるかを判定する。
こんなイメージ。 © 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(); } } }
- 何をやるにしても
JtsSpatialContext
が必要になるので、まずはこれを取得する。JtsSpatialContext.GEO
でシングルトンインスタンスが用意されているので、そのまま使用する。 - 今回はGeofenceと位置情報をWKT(Well-known text)で用意する。そのため、
WKTReader
のインスタンスを作成する。WKTの詳しい説明はウィキペディアが参考になる。 - GeofenceをPolygonで表したWKTを
WKTReader
に読み込ませて、Shape
オブジェクトを作成する。 - 比較対象とするデバイスの位置情報はPointで表す。Geofenceと同様に
Shape
オブジェクトを作成する。 - Geofenceの
Shape.relate()
を呼び出し、2つのオブジェクトの関係を求める。
実行するとこうなる。
CONTAINS DISJOINT
Shape.relate()
の戻り値はSpatialRelation
のEnum。PolygonとPointとの比較だとCONTAINS
(比較対象が含まれている)、 DISJOINT
(比較対象との接点がない)のどちらかしか返さないが、他にINTERSECTS
(比較対象と交差している)、 WITHIN
(比較対象の中にある)がある。
ちなみに、180度経線(下の図の赤い線)を跨いで領域を定義した場合、ライブラリによってはうまく結果を返さないものもある。Spatial4jは問題なく動いた。これならフィジー諸島とかロシアでもバッチリ監視できる。
円(サークル)型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(); } } }
GeoCircle
の半径はdegree(度)で指定するので、「中心から半径2km」のように指定する場合は、DistanceUtils.dist2Degrees()
で変換処理を行う必要がある。ちなみにDistanceUtils
はEARTH_MEAN_RADIUS_KM
(平均の半径?: 約6371km)とEARTH_EQUATORIAL_RADIUS_KM
(赤道半径: 約6378km)と2つの半径がある。ここではGoogle Mapの距離測定と結果が近い方のEARTH_MEAN_RADIUS_KM
を採用。- 中心と半径、
JtsSpatialContent
を指定してGeofenceを表すGeoCircle
オブジェクトを作成。
以降は先ほどと同様。実行するとこうなる。
CONTAINS DISJOINT
最後に
Big Brother is watching you.