ラベル 基盤地図情報 の投稿を表示しています。 すべての投稿を表示
ラベル 基盤地図情報 の投稿を表示しています。 すべての投稿を表示

2012/11/03

自分のための「基盤地図情報からshapefile」(2)

さて、前回は、基盤地図情報をshapefile に変換するための前準備として、
PyShp の設定方法について述べた。今回は、実際に実装を試みる。

ちなみに、自分用を前提としているので、エラー処理はしていないし、
何せ、Python を使いはじめて、9ヶ月くらい?
我流で書いているので正しい書き方かは解らない。

大したコードでは無いので、流用は自由。一言、書いて頂ける有難いが、
無断転用も含めてご自由に。ただし、一切、責任は取りませんので悪しからず。
その代わり、バグがあったら教えて下さい。

なお、pyshp のライセンスは、MITライセンスとなっている。詳細は以下にあるので、
本当に、利用したいという人は、一読しておいた方が良いかもしれない。

http://code.google.com/p/pyshp/

さてさて、では早速、今回実験したコードを見てみる。
最初に、宣言部分と今回利用したライブラリから。
このブログは自分ため。だから、親切な解説はしない。解らない人は自力で。

ソースコードは以下からダウンロード可能(2012年12月11日現在)。
https://docs.google.com/open?id=0B9OtIs5BjRVha1VmemsxTVAta2s

最初は、クラスの定義から。色々と書いていくと、どうも煩雑になってしまう。
そのため、今回は、UMLクラス図を使って、クラスを整理してみた。
まずは、基本となるGMLオブジェクトに関わるクラス。


ここでは、GMLの「幾何オブジェクト」と「時間オブジェクト」を定義している。
地理情報標準の空間スキーマ(ISO19107)に基づいているので、少々複雑。
しかも、突貫で作っているので、多少の不都合はあるが、今はこの状態で。

これらのクラスの最大の役割は、GML形式のXML要素が与えられた時に、
必要な属性情報を、入力された要素から抽出しインスタンス化すること。
クラスとして定義することで、汎用的に使えるようになっている。

このクラス図が読めることが前提なので、細かい説明はしない。
いずれ、APIリファレンスも作る予定なので、併せてみれば理解できるハズ。
UMLもPython も解らない人は、少々、厳しいか。

あえて、注釈をつけておくとしたら、Gmlクラスの役割について。
ネームスペースの部分を毎回書くのが面倒なので、定数として定義しているだけ。
以下のように実装してある。

class Gml(object):
__slots__ = ("__nsGml")

def __init__(self):
self.__nsGml = '{http://www.opengis.net/gml/3.2}'

def __getNsGml(self):
return self.__nsGml

getNsGml = property(__getNsGml)


一応、簡単な解説のみ。class Gml(object): の部分でGmlというクラスを定義し、
このクラスが、Python におけるObject型を継承しているを示している。

__slots__ の部分では、このクラスで利用可能な属性名を限定している。
Python という言語、かなり奇妙な言語で、勝手に属性を加えることができてしまう。
この機能を封じるために、__slots__の部分で使用する属性を限定する。

__init__ の部分は、このクラスが実装された際に、行う初期化処理。
__nsGml という属性に、'{http://www.opengis.net/gml/3.2}' という値を代入している。
属性前に、「__」 が付いているのは、この属性がプライベートになっていることを示す。

属性の読み書きは、setter と getter で設定することができる。ちなみに、
__nsGml 属性は、getterのみを設定している。つまり、読み取り専用属性。
最初に初期化して以降、この値を変更することはできない。

この属性を外部から読む時の名前、つまり属性名は、getNsGml であり、
外から呼び出すときには、この名前を使って、{http://www.opengis.net/gml/3.2}
という文字列を取得する。

他のクラスに関しては...ダラダラと書いているだけなので、説明する必要はなかろう。

次に、基盤地図情報をマッピングするためのクラスの定義。
まぁ、基本的には、このような感じになる。「やっつけ仕事」で実装したので、
製品仕様書を確認しながら修正した方が良さそうである。現段階は、実験段階。



ここで定義されているクラスも、基本的には、GML形式のデータを読み込み、
入力されたXML要素から、属性情報を抽出するのが主たる目的である。
これに加えて、Shapefile に出力機能を持たせている。

現段階では、行政区画(AdministrativeArea)、行政区画代表点(AdministrativePoint)、
行政区画線(AdministrativeBoundary)、道路構成線(LoadEdge)の4クラスを実装できるようになっていて、
それぞれ、これらのクラスを集約するクラスを持っている。

3つの集約クラス、AdministrativeAreas、AdministrativePoints、AdministrativeBoundaries、
は、複数のインスタンスを集約するのが目的。このクラスが、shapefile への出力や、
XML形式のデータを読み込んだり、といった処理をまとめて行う。

重要な点は、これら3つのクラスの初期化の部分。基本的に、どれも同じなので、
以下は、AdministrativePoints の初期化部分の例。

class AdministrativePoints(JPGISClass):
__slots__ = ("__admPts", "__shp")

def __init__(self, shape = None):
JPGISClass.__init__(self)
self.__admPts = set()
self.__shp = shapefile.Writer(shapefile.POINT)
self.__shp.field('FID')
self.__shp.field('LFSPANFR')
self.__shp.field('DEVDATE')
self.__shp.field('ORGGILVL')
self.__shp.field('ORGMDID')
self.__shp.field('VIS')
self.__shp.field('TYPE')
self.__shp.field('NAME')
self.__shp.field('ADMCODE')

初期化する際に、PyShp のshapefile オブジェクトを実装し、
さらに、このタイミングで、shapefile の形式と、
必要な属性(フィールド)を設定しておく。

いずれ、shapefile からのインスタンス化も、と考えていて、
引数に、shape=None を定義しているが、このコードを見て解る通り、
現段階では、全く意味を成していない。

それは、まぁ、良いことにする。


さらに、3つのクラスを集約するクラスは、「JPGISClass」というクラスを継承している。
このクラスでは、唯一、convertEncという関数が定義されているだけ。
この関数は、GML形式のXMLデータのエンコードをShift-JISからUTFに変換する。

基盤地図情報は、Shift-JIS を使っているが、そのおかげで色々と厄介な問題が発生する。
UTF-8に変換するための処理がここで定義されている。Shift-JIS は、やっぱり困る。
このクラスを実装すると、以下のようになる。

class JPGISClass(object):
def convertEnc(self, filename):
# Read a original xml file as a text file format.
txt = open(filename).read()

# Convert the text file to unicode.
unic = unicode(txt, 'cp932')

# Convert the encoded unicode text file to the UTF-8 file.
utf8 = unic.replace("Shift_JIS", "utf-8").encode('utf-8')

# Return converted text string.
return utf8



とにかく、この部分の処理が重要。Python は、日本語には優しくない。
特に、Shif-JIS というローカライズド・エンコーディングの扱いは大変。
ファイルサイズの問題があるとは言え、この点は、何とかしてもらいたいものである。

さて、ダラダラと、各クラスとその説明を書いても仕方ないので、
ここで、全体的な処理の流れを確認してみる。
ダウンロード済みのデータが、以下のようになっているとして...

/path/to/download/PackDLMap
├── FG-GML-14101-05-Z001.zip
├── FG-GML-14101-06-Z001.zip
├── FG-GML-14101-08-Z001.zip
     ・
     ・
     ・
├── FG-GML-14118-05-Z001.zip
├── FG-GML-14118-06-Z001.zip
└── FG-GML-14118-08-Z001.zip


今回、書いたPython スクリプトの実行方法は以下の通り。

python cis.jpgis.py -d '/path/to/download/PackDLMap' 

このように、するだけで、ファイルの解凍も含めて自動処理してくれる。
我ながら、便利なツール。これができれば、QGIS から直接読み込むこともできる。
まぁ、QGISを使いたいのであれば、別の方法もあるらしいが。

さて、このスクリプトが、実際に何をしているかというと、
def main(argv): で定義されている部分が、最初に動作しはじめて、
zipファイルの解凍、xmlデータのパース、shpefileへの書き出しを順次行う。



要するに、別のルーチンに飛んで処理を行なっているだけ。
個々の処理は、それほど、複雑では無いので、まぁ、見たら解るだろう。
そもそも、これは、あくまで「自分のため」のメモ程度。

さてさて、最終的に上手く出力できると、以下のような図が出てくるハズ。
この図は、横浜市のデータを一括ダウンロードして、shapefile に変換したもの。
同様に、クラスを拡張すれば、他のデータも変換できるハズ。
ただし、一点だけ、注意すべきことがある。
現在のところ、ドーナツポリゴンが処理できていない...。
PyShp の仕様を読んでも良く解らない。

やはり、PostGIS に実装した方が良いのかもしれない。
それにしても、shapefile は、どうも使い勝手が悪いので、好きに慣れない。
DBに入れる方が、早いかもしれない。

2012/10/30

自分のための「基盤地図情報からshapefile」(1)

GISを日常的に使っている人で、「基盤地図情報」を知らない人は居まい。
日本全国の基本的なGISデータは、ここで入手するのが一般的。
最近では、OSMの方を利用している人も多いようであるが...。

基盤地図情報は、GISの国際標準「ISO19100シリーズ」という標準に準拠していて、
国土地理院からは、XML形式のファイルをダウンロードして利用できる。
まぁ、ここまでは、どこにでも書いている話。

2012年10月30日現在、ダウンロード可能なXMLファイルは、
GML形式(ISO19136)と、符号化規則「附属書8」形式(ISO19118)の二種類の形式で、
何れかの形式のものをダウンロードして変換しGISソフトで利用する。

この変換には、通常、国土地理院の「基盤地図情報閲覧コンバートソフト」を使うが、
実は、このツールは、Windows専用であり、Mac ユーザとUbuntuユーザは苦労する。
ということで、今回は、OSに依存しないツールの開発を試みる。

今回は、Ubuntu上で、Pythonを使って変換する方法を考えてみる。
大雑把な流れは、次の通り。
  1. 最初にXMLの要素をパースする。
  2. 基盤地図情報クラスをインスタンス化する。
  3. shapefile に変換する。
2.は必ずしも必要は無いが、再利用を考えるとこの方が良い。
問題は、shapefile に変換する部分であるが、この部分に関しては、
pyshp というライブラリを使う。とりあえず、インストール。

インストールは、easy_install というツールを利用するのが便利であるが、
このツールを利用するためには、予め、python-setuptools が必要となる。
念の為に、このツールのインストール方法もメモしておく。

sudo apt-get install python-setuptools
sudo easy_install pyshp

さて、実は、 pyshp というライブラリ、日本語が入ると色々とエラーが発生し、
上手く、shapefile に変換することができない。
そこで、インストールしたソースを修正し、日本語に対応できるようにする。

まず、インストールされているソースを開く。
バージョンが変わっているかもしれない。注意する。
とりあえず、この記事の執筆時点では、以下のパスの下に存在していた。

/usr/local/lib/python2.7/dist-packages/

もちろん、インストールの仕方によって場所は異なる可能性がある。
まぁ、この記事を読んでいる人であれば、余計な話か。
とにかく、編集する。「vi」と言いたいところであるが「gedit」での編集。

sudo gedit /usr/local/lib/python2.7/dist-packages/pyshp-1.1.4-py2.7.egg/shapefile.py

最初に修正するべき箇所は、エンコーディングのオマジナイ。
とにかく、以下の二行を、ソースの先頭に入れておく。

#!/usr/bin/python
# -*- coding: utf-8 -*-

次に、エラー回避を行うためのコードを追加。376行目辺りとなるか?
def __dbfRecords(self): という関数の部分を修正する。
size = int(size) の後ろに、value = str(value.encode("utf-8")) を追加するだけ。

ようするに、エンコーディングエラーの対策。
とりあえず、以下のようになる。

def __dbfRecords(self):
"""Writes the dbf records."""
f = self.__getFileObj(self.dbf)
for record in self.records:
   if not self.fields[0][0].startswith("Deletion"):
f.write(b(' ')) # deletion flag
   for (fieldName, fieldType, size, dec), value in zip(self.fields, record):
fieldType = fieldType.upper()
size = int(size)
value = str(value.encode("utf-8"))
if fieldType.upper() == "N":
   value = str(value).rjust(size)
elif fieldType == 'L':
   value = str(value)[0].upper()
else:
   value = str(value)[:size].ljust(size)
assert len(value) == size
value = b(value)
f.write(value)

これで準備完了。あとは、変換するためのプログラムを書くだけ。
自分でコーディング人は、ここまでの情報があれば良いかもしれない。
ということで、次回は、実際にコーディングをしてみる。