Django Rest Framework GISで誰でも簡単RESTful Geo API
※この投稿は、FOSS4G Advent Calendar 2015 - Qiitaの15日目の記事です。
こんにちは、本格派アフロの非本格派プログラマmonomotiです。久しぶりにFOSS4G Advent Calendarに参加します。
はじめにお断りしておきます。当記事には残念ながら何一つ新しい/オリジナルなネタはございません。
便利なジオ系フレームワークのご紹介です。周りにGeoDjangoを使っている人がいなくて、かなり寂しいので、ユーザを増やしてキャッキャしたいなというだけです。
公式ドキュメントに書いてある事ばかりですが、「あちこち英語のドキュメントを見るのがめんどくさい」という方が使い始める切っ掛けになれば幸いです。
さて、今回ご紹介するのは、PythonのWebアプリケーションフレームワークDjangoの地理空間系モジュールであるGeoDjangoと、Django Rest Framework GISを用いた、RESTfulなWeb APIの作り方です。
作成するAPIは、httpクライアントでjsonフォマットのデータを送受信して、サーバの地理空間データの作成・取得・更新・削除を行うものとします。
また、RESTfulとはなんぞや、という件についてはこちら(https://ja.wikipedia.org/wiki/REST)をご覧下さい。
以下、pyenv + virtualenvでpython3.4、データベースはPostgreSQLとPostGISがインストール済みという環境を前提に、ご説明します。
GeoDjangoのセットアップと初期データの準備
では、本題のAPI作成の前に、GeoDjangoのチュートリアルの流れに沿って、さっくりとGeoDjangoアプリケーションの作成から初期データの準備までやってしまいます。Djangoをインストールします。
$ pip install django >> Collecting django >> Using cached Django-1.8.7-py2.py3-none-any.whl >> Installing collected packages: django >> Successfully installed django-1.8.7
データベースを作成します。名前はAdvent Calendar 2015ということでac2015にします。
$ createdb -U postgres -E UTF-8 ac2015 $ psql -U postgres -d ac2015 -c "CREATE EXTENSION postgis;" >> CREATE EXTENSION
適当なディレクトリを作ってそこに移動し、Djangoのプロジェクトを作成します。プロジェクトの名前は何でもよいですが、ここではチュートリアルと同じgeodjangoにします。
$ mkdir blog20151215 $ cd blog20151215 $ django-admin startproject geodjango
このようなディレクトリ・ファイル群が作成されます。
geodjango/ ├── geodjango │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py
geodjangoに移動します。
$ cd geodjango
Webアプリケーションを作成します。ここも名前は何でもよいですが、ac2015にします。
$ python manage.py startapp ac2015
すると一番上のgeodjangoディレクトリ以下、こういうディレクトリ・ファイル群になります。
geodjango/ ├── manage.py ├── geodjango │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ └── ac2015 ├── __init__.py ├── admin.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py
以降、一番上のgeodjangoディレクトリからの相対パスで説明します。
geodjango/settings.pyを開いて、DATABASESとINSTALLED_APPSを次のように修正します。
# geodjango/settings.py DATABASES = { 'default': { 'ENGINE': 'django.contrib.gis.db.backends.postgis', 'NAME': 'ac2015', 'USER': 'postgres', 'HOST':'localhost', } } #'django.contrib.gis'と'ac2015'を追加。 INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.gis', 'ac2015', )
初期データをインポートします。
チュートリアルのWorldBorderだとMultiPolygonでAPIの説明がちょっと面倒になるので、ここではこんな感じの自作のPointデータを使用します。僕が「行きたい飲食店リスト」です。
gist.github.com
まずはDBとアプリケーション間のデータを仲介する為の、データモデルをac2015/models.pyに定義します。
#ac2015/models.py from django.contrib.gis.db import models class IkitaiOmise(models.Model): name = models.CharField(max_length=50) category = models.CharField(max_length=50,null=False,blank=True,default="") geom = models.PointField(srid=4326) def __str__(self): return self.name
下記コマンドでマイグレーションファイルを作成します。マイグレーションファイルとは、データモデルの変更内容をデータベースに反映させる為の命令を記述するファイルです。
#実行時、pythonのライブラリ(psycopg2等)が無いと怒られるかもしれません。その場合は適宜pipでインストールして下さい。
$ ./manage.py makemigrations >> Migrations for 'ac2015': >> 0001_initial.py: >> - Create model IkitaiOmise
下記コマンドで、データモデルの変更内容をデータベースに反映させます。これをマイグレーションと言います。
$ ./manage.py migrate >> Operations to perform: >> Synchronize unmigrated apps: messages, staticfiles, gis >> Apply all migrations: sessions, ac2015, contenttypes, admin, auth >> Synchronizing apps without migrations: >> Creating tables... >> Running deferred SQL... >> Installing custom SQL... >> Running migrations: >> Rendering model states... DONE >> Applying ac2015.0001_initial... OK >> Applying contenttypes.0001_initial... OK >> Applying auth.0001_initial... OK >> Applying admin.0001_initial... OK >> Applying contenttypes.0002_remove_content_type_name... OK >> Applying auth.0002_alter_permission_name_max_length... OK >> Applying auth.0003_alter_user_email_max_length... OK >> Applying auth.0004_alter_user_username_opts... OK >> Applying auth.0005_alter_user_last_login_null... OK >> Applying auth.0006_require_contenttypes_0002... OK >> Applying sessions.0001_initial... OK
インポートするgeojsonファイル(ikitai_omise.json)を適当な場所配置します。どこでも良いのですが、ここではac2015/dataにしておきます。
ac2015ディレクトリの下に、データのインポートをするスクリプトファイルを作成します。ここではチュートリアルと同じく、load.pyという名前にします。
#load.py import os from django.contrib.gis.utils import LayerMapping from ac2015.models import IkitaiOmise ikitai_omise_mapping = { 'name' : 'name', 'category' : 'category', 'geom' : 'POINT', } ikitai_omise_geojson = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data', 'ikitai_omise.json')) def run(verbose=True): lm = LayerMapping(IkitaiOmise, ikitai_omise_geojson, ikitai_omise_mapping, transform=False, encoding='UTF-8') lm.save(strict=True, verbose=verbose)
djangoのshellで、上で作成したスクリプトを実行します。これでikitai_omise.jsonの内容がデータベースにインポートされます。
$ ./manage.py shell >>> from ac2015 import load >>> load.run() >>> ... >>> Saved: インドラディップ INDRADIP >>> Saved: Buci boccheno >>> Saved: Pie and Beer PUB ハバ >>> Saved: トタンにライスカレー >>> Saved: いちにいさん >>> Saved: らーめん処さんさん >>> Saved: (株)ダンディー 二号店 >>> Saved: ベビーフェイスプラネッツ 橿原店 >>> Saved: 葛城ガーデン >>> Saved: アローム >>> Saved: 奈良県葛城市當麻 中将堂本舗 >>> Saved: エアーズカフェ岡谷店 >>> Saved: 麺屋 誠和 >>> Saved: Barm's >>> Saved: 浜勝 >>> Saved: お好み鉄板がるぼ >>> Saved: 二刀流
このdjango.contrib.gis.utils.LayerMapping、超便利です。もう少し突っ込んだ使い方については、また別の機会にご紹介したいと思います。
シェルを終了します。
>>> exit() $
Djangoのとても便利な点の一つは、最初からデータ管理画面が用意されているので、何もプログラミングしなくてもデータの入力・編集を行える事です。
先ほどロードしたデータをこのデータ管理画面で確認してみましょう。
ac2015/admin.pyを下記の通りに修正します。
#ac2015/admin.py from django.contrib.gis import admin from ac2015.models import IkitaiOmise admin.site.register(IkitaiOmise, admin.GeoModelAdmin)
geodjango/urls.pyの
from django.contrib import admin
を
from django.contrib.gis import admin
に修正します。
djangoアプリケーションの管理者権限を持つユーザを作成します。
$ ./manage.py createsuperuser >> Username (leave blank to use 'myname'): myname >> Email address: mymail@mymail.com >> Password: >> Password (again): >> Superuser created successfully.
djangoアプリケーションを起動します。
$ ./manage.py runserver >> December 15, 2015 - 07:09:57 >> Django version 1.8.7, using settings 'geodjango.settings' >> Starting development server at http://127.0.0.1:8000/ >> Quit the server with CONTROL-C.
Webブラウザで http://127.0.0.1:8000/adminを開き、先ほど作成したユーザ名とパスワードでログインします。
「Ac2015」の 「Ikitai omises」をクリックすると、ロードしたデータのリストが表示されます。
リスト中のデータのリンクをクリックすると、詳細が表示されます。
geomフィールドのデータがOpenLayersで表示されています。
djangoアプリを終了する時は、コンソールでCONTROL-Cを入力します。
超簡単!RESTful APIの作成
さて、やっと本題です。RESTfulなジオAPIを作成します!Django Rest Frameworkのジオ拡張であるdjango-rest-framework-gisと、検索機能で使うdjango-filterをインストールします。
また、APIの実行結果をブラウザで表示する為に、markdownもインストールします。
$ pip install djangorestframework-gis $ pip install django-filter $ pip install markdown
ac2015/setting.pyのINSTALLED_APPAに、'rest_framework'と'rest_framework_gis'を追加します。
#ac2015/setting.py INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.gis', 'ac2015', 'rest_framework', 'rest_framework_gis', )
シリアライザを作成します。シリアライザとは、データベースのレコードとAPIで送受信するデータフォーマットの(ここではjson)の仲介役の事です。シリアライザは、APIでデータを呼び出す時はレコードの内容をjsonにして返してくれます。APIでjsonを送信すると、jsonの内容で既存のレコードが更新されたり、新しいレコードが作られたりします。
ac2015ディレクトリの下に、下記内容のserializers.pyを作成します。
#ac2015/serializers.py from rest_framework.serializers import ModelSerializer from ac2015.models import IkitaiOmise class IkitaiOmiseSerializer(ModelSerializer): class Meta: model = IkitaiOmise
ここで作成したのは、rest_frameworkが提供するModelSerializerのサブクラスです。ac2015/models.pyで定義したデータモデル(IkitaiOmise)とこのシリアライザを関連づけています。やっていることはこれだけです。
次に、REST Frameworkのビューを作成します。ここでいうビューとは、httpリクエストに対してのどう振る舞って、どういうレスポンスを返すのかを定義するもののことを言います。
もう少し具体的に言うと、「データを更新するのか、作成するのか、削除するのか、1取得するのか、複数取得するのか、複数取得するならどういう絞り込みをするのか」という事を定義します。
ここでは、複数件を取得する時は、任意の緯度経度から任意の距離内にあるお店だけを取得するビューを定義してみます。
上のほうでac2015アプリケーションを作成したときに、ac2015/view.pyというファイルが作成されているので、これを下記のように編集します。
# ac2015/view.py from rest_framework_gis.filters import DistanceToPointFilter from rest_framework import generics,viewsets from ac2015 import models,serializers from rest_framework.pagination import PageNumberPagination class MyPagination(PageNumberPagination): page_size_query_param = 'page_size' class IkitaiOmiseViewSet(viewsets.ModelViewSet): queryset = models.IkitaiOmise.objects.all() serializer_class = serializers.IkitaiOmiseSerializer pagination_class = MyPagination filter_backends = (DistanceToPointFilter,) distance_filter_field = 'geom' distance_filter_convert_meters = True
ここでやっているのことを大まかに説明すると、以下の通りです。
- serializer_classで、どのシリアライザを使うかを設定。
- pagination_classで、どういうページングをするかを設定。ここではpage_sizeでURLクエリパラメータでページングのサイズを指定出来るPageNumberPaginationクラスのサブクラス(MyPagination)を使っています。
- filter_backendsで、データを絞り込む方法を設定。ここでは、指定した点からの距離で絞り込むDistanceToPointというフィルタを使っています。
- distance_filter_fieldで、データモデルのどのフィールドを絞り込みの対象とするかを設定しています。
次に上で作成したビューを、urlと対応付けます。
まず、geodjango/urls.pyを次のように修正します。
#geodjango/urls.py from django.conf.urls import include, url from django.contrib.gis import admin urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'^', include('ac2015.urls',namespace='ac2015')), ]
つぎに下記内容のac2015/urls.pyを作成します。
from django.conf.urls import patterns, include, url from rest_framework import routers from ac2015 import views router = routers.DefaultRouter() router.register(r'places', views.IkitaiOmiseViewSet) urlpatterns = patterns('', url(r'^api/', include(router.urls)), )
これでビューとurlの関連付けが出来ました。
そしてこれで、地理空間データの作成・取得・更新・削除を行うRESTfulなAPIが出来ました。
はい、これだけです。超簡単!
さっそくAPIを試してみましょう。
djangoアプリケーションを起動します。
$ ./manage.py runserver
それでは、近年話題のウラなんば、緯度34.66542、経度135.50324の半径300m内にあるお店を検索してみましょう。
Django Rest API Frameworkは、APIをwebブラウザでテストする為のUIが用意されているので、いちいちコンソールから
$ curl -X GET -H "ContentType:application/json" http://localhost:8000/api/places/?dist=300&point=135.50324,34.66542
なんてタイプする必要がありません。Webブラウザで、下記のURLを開いてみて下さい。
http://localhost:8000/api/places/?dist=300&point=135.50324,34.66542
お店が出てきました。超簡単でしょう?
全くプログラミングはしていないのに、ちょっとビューを設定するだけでここまで出来てしまいます。
ごらんのとおり、位置情報であるgeomカラムのデータは、GeoJSONのgeometry属性のフォーマットで表現されています。
{ "id": 67, "name": "かき小屋フィーバー ボーボーアフロ", "category": "レストラン", "geom": { "type": "Point", "coordinates": [ 135.505652, 34.664452 ] } }
「仕様でPointだと分かっているのに"type": "Point"は冗長だ」とか「Leafletで扱いやすい[lat,lng]の方がいい」という場合は、シリアライザのto_representation(シリアライズ側)とto_internal_value(デシリアライズ側)というメソッドに手を加えれば、
{ "id": 67, "name": "牡蠣小屋フィーバー ボーボーアフロ", "category": "レストラン", "geom": [ 34.664452, 135.505652 ] },
という様にすることができます。ですが今回は「プログラミング無し!」にしたいので、このままにします。
検索によってデータのIDが分かったので、任意の1件取得してみましょう。
http://localhost:8000/api/places/{id}/ の書式でIDを指定します。
例えば、id=67なら、下記URLになります。
http://localhost:8000/api/places/67/
あれ?お店の名前が間違っている事に気づきました。大変失礼致しました。「牡蠣小屋」ではなく「かき小屋」ですので修正します。
データの更新はPUTメソッドです。PUTボタンクリックすると...
修正されました。
次はデータの追加を試してみましょう。
谷町九丁目駅から1km内で検索してみると(http://localhost:8000/api/places/?dist=1000&point=135.5157,34.66647)...
お店が1件しか無いのは寂しいので、追加します。ブラウザからでも出来ますが、Web APIであることを実感するために、こんどはコンソールからやってみましょう。
次のコマンドをコピー&ペーストして実行してみて下さい。
curl -X POST -H "Accept: application/json" -H "Content-type: application/json" -d '{"name": "旧ヤム邸宅","category": "レストラン","geom":{"type": "Point","coordinates": [135.5129899,34.6736539]}}' http://localhost:8000/api/places/
下のような応答があれば成功です。
>> {"id":109,"name":"旧ヤム邸宅","category":"レストラン","geom":{"type":"Point","coordinates":[135.5129899,34.6736539]}}
先ほどの検索結果をリロードしてみましょう。
新しいデータがid=109で作成されていますね。
因に、検索の際、結果の件数が多かったのでページングはしていていませんでしたが、件数が多い場合はページングするのが普通です。
ページングは、URLクエリパラメータにpage_size={1ページ当たりの件数}&page={ページ番号}を追加する事で行います。
例えば、大阪梅田から1km内のお店を10件ずつ取得したい場合は、URLは
http://localhost:8000/api/places/?dist=1000&point=135.4953,34.7022&page_size=10&page=1
http://localhost:8000/api/places/?dist=1000&point=135.4953,34.7022&page_size=10&page=2
のようになります。
さて、ここまで任意の点からの距離で検索してきましたが、他にもいろいろなフィルタリング方法が用意されています。
最後にバウンダリでのフィルタリングをご紹介します。
ac2015/views.pyを下記のように編集します。
# ac2015/views.py from rest_framework_gis.filters import DistanceToPointFilter, InBBoxFilter # <=ここを変更 from rest_framework import generics,viewsets from ac2015 import models,serializers from rest_framework.pagination import PageNumberPagination class MyPagination(PageNumberPagination): page_size_query_param = 'page_size' class IkitaiOmiseViewSet(viewsets.ModelViewSet): queryset = models.IkitaiOmise.objects.all() serializer_class = serializers.IkitaiOmiseSerializer pagination_class = MyPagination filter_backends = (DistanceToPointFilter, InBBoxFilter) # <=ここを変更 distance_filter_field = bbox_filter_field = 'geom' # <=ここを変更 distance_filter_convert_meters = True
変更の内容は、以下のとおりです。
- filter_backendsにInBBoxFilterを追加して、バウンダリでの絞り込みも出来る様に設定。
- bbox_filter_fieldで、絞り込み対象のフィールドを設定。
では、経度135.48,緯度34.6969を南西端、経度135.4984,緯度34.7055を北東端とするバウンダリで検索してみましょう。
URLは下記の様に指定します。
http://localhost:8000/api/places/?in_bbox=135.48,34.6969,135.4984,34.7055
すごい!簡単ですね!
こんな感じで、プログラミングなしでも、GeoJSONとDjango REST Frameworkで色々出来てしまいます。
皆さん是非使ってみて下さい!
明日はZero_Kohakuさんです!
追記:
ちなみに、Django Rest Framework GISには完全なGeoJSONを扱うシリアライザがあり、TMSTileFilterというフィルタもあるので、「これでGeoJSONタイルサービスができるのかな?」と思ったのですが、ソースを見た所TMSTileFilterはその名のとおり絞り込みを行うだけで、タイル上にベクターをカットしてくれる訳ではありませんでした。
GeoJSONタイルサービスを作りたいなら、下記で紹介されているdjgeojsonというモジュールを使うのがよさそうです(1年も前の記事なのか...汗)。
PostGISとGeoDjangoを使ってLeafletでGeoJSON Tile Layerを表示してみる(5) – GeoDjangoでTiled GeoJSONを出力する – – ビットログ