SyntaxHighliter

2012年7月16日月曜日

JDBCでポリゴンを取得 後編(Google Maps V3 - 5)

やっとJDBCでアクセスする。
package com.tcf_corp.postgis.service;

import it.rambow.master.javautils.PolylineEncoder;
import it.rambow.master.javautils.Track;
import it.rambow.master.javautils.Trackpoint;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.sql.DataSource;

import org.postgis.Geometry;
import org.postgis.MultiPolygon;
import org.postgis.PGgeometry;
import org.postgis.Point;
import org.postgis.Polygon;
import org.seasar.extension.dbcp.ConnectionWrapper;

import com.tcf_corp.postgis.dto.GeoPointDto;
import com.tcf_corp.postgis.dto.LocalAreaDto;

public class LocalAreaService {
    @Resource
    private DataSource ds;

    public List<LocalAreaDto> getLocalArea(Trackpoint ne, Trackpoint sw) {
        Connection con = null;
        PreparedStatement st = null;
        ResultSet rs = null;
        List<LocalAreaDto> list = new ArrayList<LocalAreaDto>();
        try {
            con = ds.getConnection();
            // seasarはラッパーを返すのでそのままではaddDataTypeできない
            if(con instanceof ConnectionWrapper) {
                ConnectionWrapper cw = (ConnectionWrapper)con;
                Connection conn = cw.getPhysicalConnection();
                ((org.postgresql.PGConnection)conn).addDataType("geometry",org.postgis.PGgeometry.class);
            }
            // シングルクォート内は、パラメータがセットできない
            String sql = "SELECT gid, geom FROM localarea WHERE ST_Intersects(geom,ST_GeomFromText(?, 4612)) = true;";
            st = con.prepareStatement(sql);

            String arg = String.format("MULTIPOLYGON(((%s %s,%s %s,%s %s,%s %s,%s %s)))"
                    , ne.getLonDouble(), ne.getLatDouble()
                    , ne.getLonDouble(), sw.getLatDouble()
                    , sw.getLonDouble(), sw.getLatDouble()
                    , sw.getLonDouble(), ne.getLatDouble()
                    , ne.getLonDouble(), ne.getLatDouble());
            st.setString(1, arg);
            rs = st.executeQuery();

            // エンコーダ
            PolylineEncoder pe = new PolylineEncoder();
            while(rs.next()) {
                int gid = rs.getInt("gid");
                PGgeometry geom = (PGgeometry)rs.getObject("geom");
                if(geom.getGeoType() == Geometry.MULTIPOLYGON) {
                    MultiPolygon mp = (MultiPolygon)geom.getGeometry();
                    // MultiPolygonの中にはPolygonが複数ある
                    for(int i = 0 ; i < mp.numPolygons() ; i++) {
                        LocalAreaDto dto = new LocalAreaDto();
                        Track trk = new Track();
                        Polygon pl = mp.getPolygon(i);
                        dto.points = new ArrayList<GeoPointDto>();
                        // Polygonは境界をあらわすPointで構成される
                        for(int j = 0 ; j < pl.numPoints() ; j++) {
                            Point p = pl.getPoint(j);
                            Trackpoint tp = new Trackpoint(p.y, p.x);
                            trk.addTrackpoint(tp);
                            GeoPointDto gpoint = new GeoPointDto();
                            gpoint.lat = p.y;
                            gpoint.lng = p.x;
                            dto.points.add(gpoint);
                        }
                        Map<String, String> enc = pe.dpEncode(trk);
                        dto.gid = gid;
                        dto.enc = enc.get("encodedPoints");
                        list.add(dto);
                    }
                }
            }
            rs.close();
            st.close();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if(con != null) {
                try {
                    con.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return list;
    }
}
簡単に解説。
43行目のConnectionに対してaddDataTypeをしているところ。
PostGISのサンプルに書いてあるのはorg.postgresql.Connectionだがそのようなクラスはないので変更。同様に2番目の引数も文字からクラス型に変更。
その前のif文はseasar2を使っているのでコネクションのラッパーから実コネクションを使用するためのもの。

46行目のSQLは苦労するポイント。
心情的にはMULTIPOLYGONの引数もパラメータに設定したいがリテラル内なのでこれはできない。
MULTIPOLYGONから始まる文字列を渡さないといけない。

あとは、MULTIPOLYGON→複数のポリゴン→複数の点というところがわかれば問題はないと思う。

PolylineEncoderは点の集まりをエンコードして少ない文字列で表すためのものだが、うまくいっていない。
今後の課題。

このメソッドの呼び出し側で作ったリストをJSON形式にしている。
JSONはJSONICを使った。

で、今度はJavaScript側。
var map;
var local = {};
$(function(){
    var latlng = new google.maps.LatLng(35.6807428288539, 139.931797719145);
    var myOptions = {
        zoom: 15,
        center: latlng,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

    // マップが最初に起動したときのイベント
    var boundslistener = google.maps.event.addListener(map, 'bounds_changed', function(){
        getBoundsArea();
        google.maps.event.removeListener(boundslistener);
    });
    google.maps.event.addListener(map, 'idle', function(){
        getBoundsArea();
    });
//    google.maps.event.addListener(map, 'dragend', function(){
//        getBoundsArea();
//    });
});

function getBoundsArea() {
    var bounds = map.getBounds();
    var ne = bounds.getNorthEast();
    var sw = bounds.getSouthWest();
    var zoom = map.getZoom();
    var data = {
         neLat : ne.lat()
        ,neLng : ne.lng()
        ,swLat : sw.lat()
        ,swLng : sw.lng()
        ,zoom: zoom
        ,component : 'areaPage'
        ,action : 'ajaxLocalArea'
    }
    var opt = {
         type : 'post'
        ,url : 'http://localhost:8080/postgis/teeda.ajax'
        ,data : data
        ,success : function (data, dataType) {
            var arr = $.parseJSON(data);
            for(var i = 0 ; i < arr.length ; i++) {
                if(local[arr[i].gid]) {
                } else {
                    var enc = arr[i].enc;
                    var path = google.maps.geometry.encoding.decodePath(enc);
                    var points = [];
                    for(var j = 0 ; j < arr[i].points.length ; j++) {
                        points.push(new google.maps.LatLng(arr[i].points[j].lat, arr[i].points[j].lng));
                    }
                    var polygon = new google.maps.Polygon({
                         fillColor : '#0000FF'
                        ,fillOpacity : 0.1
                        ,strokeColor : '#0000FF'
                        ,strokeOpacity : 0.2
                        ,strokeWeight : 2
//                        ,path : path
                        ,path : points
                        ,map : map
                    });
                    arr[i].polygon = polygon;
                    local[arr[i].gid] = arr[i];
                }
            }
        }    
    };
    $.ajax(opt);
}
初回とidle時にデータ取得処理をしている。
getBoundsで表示をしているマップの右上と左下の座標を取得して、サーバ側の呼び出し。

返ってきたJSONでポリゴンを生成している。
マップ移動のたびに呼び出して作ると同じ場所に何度もポリゴンを作ることになり
色が濃くなっていくので怪しい判定で回避している。

いろいろとチューニングの余地があるがとりあえず表示までたどり着いた。

2012年7月15日日曜日

JDBCでポリゴンを取得 前編(Google Maps V3 - 4)

PostGISはPostgresqlの拡張パッケージなので

  • PostgresqlのJDBCドライバ
  • PostGISの追加モジュール
の2つが必要です。

まずは、PostgresqlのJDBCドライバをダウンロード。
Javaが1.6と1.7は「JDBC4を使え」ということなのでこちらをダウンロードしました。


次にPostGISのJDBCドライバなのですが、
本家サイトのリンクからは「Not Found」という状態。

postgis-2.0.0SVN.jar というファイル名で検索するとダウンロードできるサイトが見つかったのでここからダウンロードした。
本来ならPostGISのソースに入っているJDBCドライバをmakeするべきなんだろうけど。。。

今は実験段階なので深くは考えずに先に進もう。

データ取得のSQLを考えると、画面に表示されている経度と緯度の矩形(四角)に
市区町村の境界が含まれていればその境界のデータを取得すればよい。

PostGISの関数リファレンスを探すとST_Intersectsが使えそう。
最初はST_Overlapsの方かと思ったのですが、こっちは完全に含む場合でした。

まずはコマンドラインで試してみる。
SELECT gid, geom
FROM localarea
WHERE ST_Intersects(geom,
ST_GeomFromText('MULTIPOLYGON(((
 経度A 緯度A
,経度B 緯度A
,経度B 緯度B
,経度A 緯度B
,経度A 緯度A
)))', 4612)) = true;
注意しなくちゃいけないのが、MULTIPOLYGONの書式。
ひとつは四角を点であらわす時には始点と終点が一致しないといけないということ。
ついつい終点をわすれて考えてしまうので要注意。

もう一つは、MULTIPOLYGONの場合はかっこが3つ必要だということ。
POLYGONの場合はかっこは2つ必要。
MULTIPOLYGONの場合は飛び地などの複数の面をあらわすことができるので こういう書式になっているのだろう。
ちなみにMULTIPOLYGONを使っているが今回の場合は画面は飛び地がないのでPOLYGONでも大丈夫だと思う。
入っているデータがMULTIPOLYGONなのであわせているだけです。

市町村のデータをダウンロード (Google Maps V3 - 3)

PostGISデータベースができたところで今度はデータをダウンロードします。
市区町村の境界データはe-statからダウンロードできます。


ここで「平成22年国勢調査(小地域)」-「男女別人口総数及び世帯総数」あたりを選びます。
一見境界データとは関係なさそうですが、「次へ」でページを移動します。

都道府県と市区町村を選んで検索をすると
市区町村の境界データのダウンロードリンクが表示されます。

「世界測地系緯度経度・Shape形式」のリンクでダウンロードできます。
しかし、残念なことに市区町村単位でしかダウンロードできないし、
都道府県内の市区町村をすべて選んでダウンロードリンクを表示させようとしても、
なかなかレスポンスが返って来ないのでストレスがたまりまくりです。

簡単なダウンロードの方法は後で考えるとしてとりあえず2~3ダウンロードしておきます。

ダウンロードしたzipの中にあるShape形式(.shp)がデータ元になります。

shp2pgsqlコマンドを使ってShapeファイルからテーブルを作ります。
-p オプションでcreate文だけ作ります。
shp2pgsql -s 4612 -p -D -i -I -W cp932 (Shapeファイル名) (テーブル名) > (出力ファイル名)
あとは、-a オプションでShapeファイルからINSERT文を作成します。-I オプションもつけません。
shp2pgsql -s 4612 -a -D -i -W cp932 (Shapeファイル名) localarea > (出力ファイル名)
sqlを作成したらpsqlコマンドでデータを追加します。
psql -U postgres -d pgis -f h22ka12204.sql
ひとまず、データが使えるようになった。

2012年7月14日土曜日

PostGISのインストール (Google Maps V3 - 2)

Google Maps の表示ができたのでGISデータの作成をする。
今回の目標は、丁目のデータを作成してGoogle Mapsにポリゴンで表示するのが目標です。

PostGISを使うのでPostgreSqlのインストールを行う。PostGISはPostgresの拡張パッケージです。

"Installer version Version 9.1.4"のをダウンロードをしてインストール。
インストール時に注意しなきゃいけないのが、「管理者として実行」すること。
そうじゃないとDB作成のところで失敗する。

引き続きPostGISのダウンロード。私が選んだのはこれ。

POSTGRESQL 64-BIT INSTALLERS

PostGIS 2.0.0 9.1 64-bit
[+]PostGIS 2.0.1 release for PostgreSQL 9.1 64-bit (inc. GEOS 3.3.5/PROJ 4.8.0/GDAL 1.9.1 ~18Mb)
 GUIのインストーラはなく、バッチのインストーラです。
makepostgisdb_using_extensions.bat の

set PGPORT=5432
set PGHOST=localhost
set PGUSER=postgres
set PGPASSWORD=yourpasswordhere
set THEDB=example_postgis20
set PGINSTALL=C:\Program Files\PostgreSQL\9.1


あたりを編集すればOK。
ここでの注意は、PostgresSqlのデフォルトのインストール先が"Program Files"なので
素直には書き込めないこと。
とりあえず、Postgres\9.1\フォルダをフルコントロールにして実行した。

でも、エラー

C:\Program Files\PostgreSQL\postgis-pg91-binaries-2.0.1w64>"C:\Program Files\Pos
tgreSQL\9.1\bin\\psql"  -d "example_postgis20" -c "CREATE EXTENSION postgis;"
ERROR:  unsafe use of \' in a string literal
行 29:                 RAISE WARNING E'Format \'%\' is not recogniz...
                                     ^
HINT:  Use '' to write quotes in strings. \' is insecure in client-only encoding
s.
テンプレートのDB作成に失敗しているみたい。
モジュールはコピーされているので、めげずに手作業でDBを作成する。
 コマンドプロンプトから
postgres=# select * from postgis_version();
            postgis_version
---------------------------------------
 2.0 USE_GEOS=1 USE_PROJ=1 USE_STATS=1
(1 行)
OK!

Google Maps を使ってみた (Google Maps V3 - 1)

Google Maps のテンプレート


Google Mapsを使うにあたってのメモ


チュートリアルに従ってhtnlを作成。とりあえず、css と js を別ファイルにして保存。
試してみると、ちゃんと表示できる。


ここまでは、webサーバいらずでJavaScriptが動けば(セキュリティ警告に承認すれば)
ファイルシステムだけでもいける。


maps.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:te="http://www.seasar.org/teeda/extension" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<title></title>
<link rel="stylesheet" type="text/css" href="./css/maps.css" />
<script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
<script type="text/javascript" src="./js/jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="./js/maps.js"></script>
</head>
<body>
 <div id="map_canvas"></div>
</body></html>

maps.js
var map;
$(function(){
 var latlng = new google.maps.LatLng(33.9, 134.2);
 var myOptions = {
  zoom: 8,
  center: latlng,
  mapTypeId: google.maps.MapTypeId.ROADMAP
 };
 map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);

 // マップが最初に起動したときのイベント
 var boundslistener = google.maps.event.addListener(map, 'bounds_changed', function(){
  var bounds = map.getBounds();
  google.maps.event.removeListener(boundslistener);
 });
});


maps.css
html { height: 100% }
body { height: 100%; margin: 0px; padding: 0px }
#map_canvas { height: 100% }