はじめに
この記事はMIERUNE Advent Calendar 2021 21日目の記事です(すでに年が明けていますが・・・泣)。前回はGeoDjangoを触ってみよう!という記事を書いたので、今回はdjango-rest-framework-gisを触ってみようというテーマにしました。前回の記事はちょっと重めになったので、今回はライトめに書きます。
実際の業務の中では、DjangoはAPIサーバーとして利用することがほとんどで、GISデータを扱う際にはこちらのモジュールを導入することが多い印象です。
django-rest-framework-gisとは
DjangoでREST APIを実装する際には、Django REST framework(DRF)というサードパーティ製のライブラリを使います。そのDRFに地理空間機能を追加したものがdjango-rest-framework-gisになります。
ソースコードはこちらになります。
前提
この記事は、Django公式ドキュメントのGeoDjangoチュートリアルを実行したソースコードに対して、DRFおよびdjango-rest-framework-gisを追加してみて、その機能を試してみようという趣旨で書いています。
対象
- DRFはある程度触ったことがある
- django-rest-framework-gisははじめて
バージョン
python_version = "3.8"
django = "==3.2.6"
psycopg2-binary = "==2.9.1"
Dockerのバージョン
$ docker version --format '{{.Server.Version}}'
20.10.11
ソースコード/ 環境
以下に実行したソースコードを載せておきます。前回の記事で紹介したリポジトリに、django-rest-framework-gisを含めたREST APIの機能を追加しています。
https://github.com/selfsryo/GeoDjangoOfficialTutorial
環境はそのままDockerでDjango + PostGIS環境を構築したものを使います。
以下のような構成になっています。
GeoDjangoOfficialTutorial
├── .gitignore
├── docker-compose.yml
├── geodjango
│   ├── .env.sample
│   ├── Dockerfile
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── entrypoint.sh
│   ├── geodjango
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── manage.py
│   └── world
│       ├── __init__.py
│       ├── admin.py
│       ├── apps.py
│       ├── data
│       │   ├── Readme.txt
│       │   ├── TM_WORLD_BORDERS-0.3.dbf
│       │   ├── TM_WORLD_BORDERS-0.3.prj
│       │   ├── TM_WORLD_BORDERS-0.3.shp
│       │   └──  TM_WORLD_BORDERS-0.3.shx
│       ├── load.py
│       ├── migrations
│       │   ├── 0001_initial.py
│       │   └── __init__.py
│       ├── models.py
│       ├── serializerss.py
│       ├── tests.py
│       └── views.py
└── postgres
    ├── .env.db.sample
    ├── Dockerfile
    └── sql
        └── init.sql
データは前回の記事で用意したShapefileを使います。
やってみよう
準備
Pipenv環境の中にDRF、django-rest-framework-gis、django-filterをインストールします。
$ cd .../GeoDjangoOfficialTutorial/geodjango
$ pipenv install djangorestframework djangorestframework-gis django-filter
その後、コンテナを一度削除します。
$ cd ..
$ docker compose down -v 
settings.pyのINSTALLED_APPに'django.contrib.gis'と、先ほど作成したworldを追加します。なお、自分の場合はDockerを使っているので、いくつかの値を環境変数としてgeodjango/.envから読み込むようにしています。DATABASESの設定値もDockerのPostGISに接続するようにしています。以上を踏まえ、settings.pyは以下のようになりました。
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = []
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.gis',
    
    # 追加
    'django_filters',
    'rest_framework',
    'rest_framework_gis',
 
    # 作成したアプリケーション
    'world',
]
シリアライザ
次にserializers.pyを作成します。アプリケーションの直下の、world/serializers.pyという階層にして作成します。
world
├── data
│   ├── Readme.txt
│   ├── TM_WORLD_BORDERS-0.3.dbf
│   ├── TM_WORLD_BORDERS-0.3.prj
│   ├── TM_WORLD_BORDERS-0.3.shp
│   └── TM_WORLD_BORDERS-0.3.shx
│
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── serializerss.py
├── tests.py
└── views.py
内容は以下のようにします。今回はGeoFeatureModelSerializerというクラスを使いました。(公式のREADMEはこちら)
from rest_framework_gis.serializers import GeoFeatureModelSerializer
from world.models import WorldBorder
class WorldBorderSerializer(GeoFeatureModelSerializer):
    class Meta:
        model = WorldBorder
        geo_field = 'mpoly'
        auto_bbox = True
        fields = ('__all__')
GeoFeatureModelSerializerはModelSerializerのサブクラスですが、以下のような特徴があります。
- 
geo_fieldの定義が必要- 
GeometrySerializerMethodFieldとして地理空間的な処理を行った上で指定することも可能
 
- 
- GeoJSON形式でレスポンスを返す
- 
id_fieldにてidを含むか含まないか、および別フィールドを指定可能(デフォルトはpk)
- 
auto_bboxにてgeo_field(およびbbox_geo_field)から計算されたbboxの値をレスポンスに含むか含まないか指定可能
- GeoJSONのpropertiesはカスタム可能
今回は、auto_bboxをTrueにしてみました。
ビュー
次にviews.pyは以下のようにしました。ビューはDRF標準のModelViewSetを使います。
from rest_framework import viewsets
from rest_framework_gis.filters import DistanceToPointFilter, InBBoxFilter
from rest_framework_gis.pagination import GeoJsonPagination
from world.serializers import WorldBorderSerializer
from world.models import WorldBorder
class MyPagination(GeoJsonPagination):
    page_size_query_param = 'page_size'
class WorldBorderViewSet(viewsets.ModelViewSet):
    queryset = WorldBorder.objects.all()
    serializer_class = WorldBorderSerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter, InBBoxFilter)
    distance_filter_field = 'mpoly'
    bbox_filter_field = 'mpoly'
    bbox_filter_include_overlapping = True
以下django-rest-framework-gisの特徴となる点です。
- 
pagination_classに GeoJsonPaginationを指定- DRFのページネーション(PageNumberPaginationなど)とはレスポンスの形式が一部異なり、GeoJSON形式になる
 
- DRFのページネーション(
- 
フィルタリングのクラスを指定できる - 
DistanceToPointFilterの場合、指定した距離に含まれるインスタンスを返す- 
distance_filter_fieldで基準となるフィールドの指定が必要
 
- 
- 
InBBoxFilterの場合、指定されたbbox内に完全に含まれるインスタンスを返す- 
bbox_filter_fieldで基準となるフィールドの指定が必要
- 
bbox_filter_include_overlapping = Trueでbbox内に完全に含まれなくとも重なっていればそのインスタンスを返す
 
- 
 
- 
URL
次にurls.pyを更新します。今回は以下のようにします。
from django.contrib import admin
from django.urls import include, path
from rest_framework import routers
from world.views import WorldBorderViewSet
router = routers.DefaultRouter()
router.register('border', WorldBorderViewSet)
urlpatterns = [
    path('admin/', admin.site.urls),
    path('world/', include(router.urls)),
]
BrowsableAPI
設定ができたのでDRFのBrawsableAPIから確認します。Dockerのコンテナを立ち上げます。
$ docker compose up --build -d
その後データのインサートを行います。前回の記事で作成したスクリプトを実行します。
$ docker container exec -it geodjango python3 manage.py shell -c "from world import load; load.run()"
ブラウザで確認してみます。単体のGETをしてみます。ブラウザから以下にアクセスします。
以下のように、GeoJSONの形式でレスポンスが返ってきていることが確認できます。

一部省略しましたがpropetiesとbboxも含まれています。

続いてページングの確認です。以下のURLを開きます。
http://localhost:8000/world/border/?page_size=5
DRFが持つページネーションクラスとは違った形式でレスポンスが返ってきていることが確認できます。

これに、距離でフィルタをかけてみます。以下にアクセスします。
http://localhost:8000/world/border/?page_size=5&dist=1&point=138.729952,35.359455
座標を富士山に指定したので、日本のインスタンスが取得できます。

最後にbboxでフィルタをかけてみます。以下にアクセスします。
http://localhost:8000/world/border/?page_size=5&in_bbox=122.935257,24.250832,153.96579,45.486382
日本のインスタンスが持つbboxを指定しましたが、bbox_filter_include_overlapping = Trueにしているので"count": 7となっています。

このように、アクセスするエンドポイントを変えることで取得する地物をフィルタリングすることができました。
おわりに
基本的な内容でしたが、体系的にdjango-rest-framework-gisを触ることができたので良い勉強になりました。

