Play framework Advent Calendar 2014 6日目 位置情報を使ってみよう
2015/02/01
この記事はPlay framework Advent Calendar 2014の6日目として書いたものです。
最近位置情報系を使ったアプリとか多いですよね。Google MapとかIngressとか。趣味で位置情報を扱うものを作っていたのですが、サーバーサイドで位置情報、空間情報をどう扱うかで迷っていました。Playを使うとなると更に情報量が減るので情報を探すのが大変でした。
なので調べた内容を適当に書いておきます。
今回の目標
- DBにいくつか都市などの位置情報を入れておく
- 指定した位置から最も近い順に都市の情報をJson形式で出力する
使うもの
- jdk1.7
- play2(2.1) java使います
- postgresql・・・mysqlも位置情報を扱う属性はあるようですが、細かい機能を見ると機能的にpostgresql(+postGIS)に劣るようでしたのでpostgresqlを選択しました。
- postGIS
- Hibernate・・・ebeanは私には扱いきれないじゃじゃ馬でした。
- CentOS 7・・・ここらへんはお好みで。
postgresqlの準備
1 2 |
rpm -Uvh http://yum.postgresql.org/9.3/redhat/rhel-7-x86_64/pgdg-centos93-9.3-1.noarch.rpm yum install postgresql93 postgresql93-server postgresql93-libs postgresql93-contrib postgresql93-devel --disablerepo=* --enablerepo=pgdg93 |
postGISの準備
1 2 3 4 5 6 7 8 9 10 |
wget http://download.osgeo.org/postgis/source/postgis-2.1.4.tar.gz tar zxvf postgis-2.1.4.tar.gz cd postgis-2.1.4 ./configure \ --with-geos=/usr/local/bin/geos-config \ --with-pgconfig=/usr/pgsql-9.3/bin/pg_config \ --with-proj=/usr/local \ --with-proj-libdir=/usr/local/lib make make install |
それっぽいテーブルを作成する
それっぽいテーブルを作成します。postGISを入れててtemplateでpostgisなテンプレートを選択するとgeometryカラムなどなど位置情報系を扱うことが出来るようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
-- DROP TABLE spots; CREATE TABLE spots ( id bigserial NOT NULL, name character varying(128) NOT NULL, ruby character varying(128) NOT NULL, "position" geometry, CONSTRAINT id PRIMARY KEY (id) ) WITH ( OIDS=FALSE ); |
データを入れる
1 2 3 4 |
INSERT INTO spots (id, name, ruby, position) VALUES (1, '東京', 'とうきょう', ST_GeomFromText('POINT(35.6809772 139.766933)', 4326)); INSERT INTO spots (id, name, ruby, position) VALUES (2, '大阪', 'おおさか', ST_GeomFromText('POINT(34.7024078 135.4959068)', 4326)); INSERT INTO spots (id, name, ruby, position) VALUES (3, '名古屋', 'なごや', ST_GeomFromText('POINT(35.1707368 136.8826952)', 4326)); INSERT INTO spots (id, name, ruby, position) VALUES (4, 'ニューヨーク', 'にゅーよーく', ST_GeomFromText('POINT(40.679269 -73.9921684)', 4326)); |
SQL文で検索もやってみましょう。長崎から近い順をのレコードを検索します。
1 2 3 4 |
SELECT * FROM spots --WHERE ST_Distance_Sphere(position, ST_GeomFromText('POINT(' || 32.7525212 || ' ' || 129.8718717 || ')', 4326)) < 1000 ORDER BY ST_Distance_Sphere(position, ST_GeomFromText('POINT(32.7525212 129.8718717)', 4326)) OFFSET 0 LIMIT (5); |
WHERE句では長崎の座標である(32.76525212, 129.8718717)から周辺(<1000)にあるレコードを取り出しています。レコードが増加した場合、検索の範囲を狭める事は重要ですが、今回はたったの4レコードですのでコメントアウトしています。
ORDER BY句では長崎の座標(32.76525212, 129.8718717)から最も近い順のレコードを取り出しています。
結果は以下。長崎から近い順に結果が表示されています。日本地図と世界地図がなんとなく頭に入っている方なら正しい結果であることがわかると思います。
1 2 3 4 |
2;"大阪";"おおさか";"0101000020E610000048BAB07FE8594140A6FBEF77DEEF6040" 3;"名古屋";"なごや";"0101000020E6100000A21C16B4DA954140C20A010A3F1C6140" 1;"東京";"とうきょう";"0101000020E61000002CA9C9422AD74140242713B78A786140" 4;"ニューヨーク";"にゅーよーく";"0101000020E6100000E3175E49F2564440FA87E3AF7F7F52C0" |
Playframework2(Java)なアプリを作る
さて、ここからがメインです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
activator new Picked up _JAVA_OPTIONS: -Dfile.encoding=SJIS Fetching the latest list of templates... Browse the list of templates: http://typesafe.com/activator/templates Choose from these featured templates or enter a template name: 1) minimal-akka-java-seed 2) minimal-akka-scala-seed 3) minimal-java 4) minimal-scala 5) play-java 6) play-scala (hit tab to see a list of all templates) > 5 Enter a name for your application (just press enter for 'play-java') > gis-test OK, application "nap" is being created using the "play-java" template. |
eclipse使うのでactivator eclipseを実行
1 |
activator eclipse |
eclipseを起動して作ったプロジェクトをインポート。とりあえずrunして動くことを確認。めちゃくちゃ時間かかるのでモンハンをして待ちます。
1 |
activator run |
起動したらアクセスできるところまで確認します。この流れはお決まり。
1 |
http://localhost:9000/ |
まずはpostgresqlとhibernate、geometryが使えるようにライブラリの追加します。
1 2 3 4 5 6 7 8 9 |
libraryDependencies ++= Seq( "org.hibernate" % "hibernate-entitymanager" % "4.2.2.Final", "com.google.inject" % "guice" % "3.0", "org.postgresql" % "postgresql" % "9.3-1102-jdbc41", javaJdbc, javaEbean, cache, javaWs ) |
次にlibsディレクトリを作成して、ファイルをいくつか配置します。またeclipseを使ってプロジェクトディレクタリに「lib」ディレクトリを作成して以下のファイルを配置。更にプロパティの「Java Build Path → Add Jars」からインポートしておきましょう。
(「libs」とか「library」とかてきとーなディレクトリ名ではダメです。playframeworkのコンパイル時に「lib」ディレクトリの中身だけは管理外依存[unmanaged]として扱ってくれます。)
- hibernate-spatial-4.0-M1.jar
- jts-1.8.jar
- jtsio-1.8.jar
- mapfish-geo-lib-1.3-SNAPSHOT.jar
- postgis-jdbc-2.0.0.jar
主にGeometry型をJavaで扱うのに必要(らしい)もの一式です。
次にapplication.confにDBの設定を追加します。ユーザー名とパスワードは適宜置き換えてください。
1 2 3 4 5 6 7 8 9 10 |
# db.default.driver=org.h2.Driver # db.default.url="jdbc:h2:mem:play" # db.default.user=sa # db.default.password="" db.default.driver=org.postgresql.Driver db.default.url="jdbc:postgresql://localhost:5432/nap" db.default.user=your_user_name db.default.password="your_password" db.default.jndiName=DefaultDS jpa.default=defaultPersistenceUnit |
次にpersistence.xmlを配置します。confディレクトリの中にMETA-INFディレクトリを作成して、その中に以下が記述されたpersistence.xmlを配置しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="defaultPersistenceUnit" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <non-jta-data-source>DefaultDS</non-jta-data-source> <properties> <property name="hibernate.dialect" value="org.hibernate.spatial.dialect.postgis.PostgisDialect" /> <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" /> <property name="hibernate.connection.pool_size" value="5" /> <property name="hibernate.show_sql" value="true" /> <property name="hibernate.format_sql" value="true" /> <property name="hibernate.max_fetch_depth" value="5" /> <property name="generateDdl" value="true" /> </properties> </persistence-unit> </persistence> |
次にdto(entites)ファイルを作成します。先ほど作成したテーブル定義に合わせます。以下はimport文やGetter/Setterなんかは省いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Entity @Table(name = "spots") public class Spot { @Id @Column(name = "id") private Long Id; @Column(name = "name") private String name; @Column(name = "ruby") private String ruby; @Type(type = "org.hibernate.spatial.GeometryType") @Column(name = "geometry") private Geometry geometry; public static Spot findById(Long geoId) { return JPA.em().find(Spot.class, geoId); } } |
次にSQLを実行するDaoクラスを作成します。(setParameterを使うと何故かExceptionを吐いてくれるので今回は雑な書き方になってます。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class SpotDao { @SuppressWarnings("unchecked") static public List GetBusstopNameDataFromPos(Double lat, Double lon) throws Exception { try { EntityManager em = JPA.em(); Query q = em .createNativeQuery( "SELECT id, name, ruby, position " + "FROM spots " + "ORDER BY ST_Distance_Sphere(position, ST_GeomFromText('POINT(" + lat.toString() + " " + lon.toString() + ")', 4326)) OFFSET 0 LIMIT (5);", Spot.class); List spots = q.getResultList(); if (spots.isEmpty()) return null; return spots; } catch (Exception e) { Logger.error("dao.Spots.IsBusstopId Exception : " + e.getMessage()); throw e; } } } |
次にroutesの設定、GETメソッドでアクセスがあったらそれっぽい値をjsonで返すようにしたいと思います。
1 2 3 |
# Home page GET / controllers.Application.index() GET /api/neighborSpots/:lat/:lon/:num controllers.Application.getNeighborSpots(lat :Double, lon:Double, num: Integer) |
最後に対応するコントローラーを追加します。Geometry型はJson.perseで変換できませんのでJsonに変換するメソッドを作成しました。(実際はModelに書いたほうが良いかもしれません。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public class Application extends Controller { public static Result index() { return ok(index.render("Your new application is ready.")); } @Transactional public static Result getNeighborSpots(Double lat, Double lon, Integer num){ if(num < 1) return badRequest(); List<Spot> spots = null; try { spots = SpotDao.GetBusstopNameDataFromPos(lat, lon); } catch (Exception e) { e.printStackTrace(); } ObjectNode jsonSpots = toJsonSpots(spots); return ok(jsonSpots); } /** * Jsonに変換 * @param spots * @return */ private static ObjectNode toJsonSpots(List<Spot> spots) { if (spots == null) return null; ObjectNode json = Json.newObject(); ArrayNode array = json.arrayNode(); for (Spot spot : spots) { ArrayNode jsonGeo = json.arrayNode(); Geometry geometry = spot.getPosition(); Point point = (Point) geometry; jsonGeo.add(point.getY()); jsonGeo.add(point.getX()); ObjectNode element = Json.newObject(); element.put("id", spot.getId()); element.put("name", spot.getName()); element.put("ruby", spot.getRuby()); element.put("position", jsonGeo); array.add(element); } json.put("results", array); return json; } } |
アクセスしてみます。座標は東京付近です。
1 |
http://localhost/api/neighborSpots/35.6809772/139.766933/5 |
以下が取得出来ました
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
{ "results": [ { "id": 1, "name": "東京", "position": [ 139.766933, 35.6809772 ], "ruby": "とうきょう" }, { "id": 3, "name": "名古屋", "position": [ 136.8826952, 35.1707368 ], "ruby": "なごや" }, { "id": 2, "name": "大阪", "position": [ 135.4959068, 34.7024078 ], "ruby": "おおさか" }, { "id": 4, "name": "ニューヨーク", "position": [ -73.9921684, 40.679269 ], "ruby": "にゅーよーく" } ] } |
入力が東京付近の座標に対して結果は「東京→名古屋→大阪→ニューヨーク」と順に出力されていますね。
位置情報に関しては本当に広くて深い分野で、なかなか難しい部分もありますが、Play+位置情報処理の初めの一歩として参考にしていただければと思います。
それでは。