django-rest-framework-gisを触ってみよう

# はじめに

この記事は[MIERUNE Advent Calendar 2021](https://qiita.com/advent-calendar/2021/mierune) 21日目の記事です（すでに年が明けていますが・・・泣）。前回は[GeoDjangoを触ってみよう！](https://qiita.com/selfsryo/items/3a56b8303827121e92cb)という記事を書いたので、今回はdjango-rest-framework-gisを触ってみようというテーマにしました。前回の記事はちょっと重めになったので、今回はライトめに書きます。

実際の業務の中では、DjangoはAPIサーバーとして利用することがほとんどで、GISデータを扱う際にはこちらのモジュールを導入することが多い印象です。



# django-rest-framework-gisとは

DjangoでREST APIを実装する際には、[Django REST framework（DRF）](https://www.django-rest-framework.org/)というサードパーティ製のライブラリを使います。そのDRFに地理空間機能を追加したものが[django-rest-framework-gis](https://github.com/openwisp/django-rest-framework-gis)になります。

ソースコードは[こちら](https://github.com/openwisp/django-rest-framework-gis)になります。



# 前提

この記事は、[Django公式ドキュメントのGeoDjangoチュートリアル](https://docs.djangoproject.com/en/3.2/ref/contrib/gis/tutorial/)を実行したソースコードに対して、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
```



# ソースコード/ 環境

以下に実行したソースコードを載せておきます。[前回の記事](https://qiita.com/selfsryo/items/3a56b8303827121e92cb)で紹介したリポジトリに、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`は以下のようになりました。

```python
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は[こちら](https://github.com/openwisp/django-rest-framework-gis#geofeaturemodelserializer)）

```python
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`を使います。

```python
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形式になる

- フィルタリングのクラスを指定できる

  - `DistanceToPointFilter`の場合、指定した距離に含まれるインスタンスを返す

    - `distance_filter_field`で基準となるフィールドの指定が必要

  - `InBBoxFilter`の場合、指定されたbbox内に完全に含まれるインスタンスを返す

    - `bbox_filter_field`で基準となるフィールドの指定が必要

    - `bbox_filter_include_overlapping = True`でbbox内に完全に含まれなくとも重なっていればそのインスタンスを返す

## URL

次にurls.pyを更新します。今回は以下のようにします。

```python
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
```

その後データのインサートを行います。[前回の記事](https://qiita.com/selfsryo/items/3a56b8303827121e92cb)で作成したスクリプトを実行します。

```
$ docker container exec -it geodjango python3 manage.py shell -c "from world import load; load.run()"
```

ブラウザで確認してみます。単体のGETをしてみます。ブラウザから以下にアクセスします。

http://localhost:8000/world/border/1

以下のように、GeoJSONの形式でレスポンスが返ってきていることが確認できます。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/e91f27d2-6924-e7d4-b24f-91d90ac36120.png)


一部省略しましたがpropetiesとbboxも含まれています。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/0530d481-1a2d-88b5-f004-fd3130985b1d.png)



続いてページングの確認です。以下のURLを開きます。
http://localhost:8000/world/border/?page_size=5
DRFが持つページネーションクラスとは違った形式でレスポンスが返ってきていることが確認できます。
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/42e156dd-3643-b1ba-f724-54cc6293b088.png)


これに、距離でフィルタをかけてみます。以下にアクセスします。
http://localhost:8000/world/border/?page_size=5&dist=1&point=138.729952,35.359455
座標を富士山に指定したので、日本のインスタンスが取得できます。
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/ffe91343-4277-bf6e-da4b-008df4e1ba92.png)

最後に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`となっています。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/8a35066b-0767-932f-65d4-c4613fb0f1f6.png)

このように、アクセスするエンドポイントを変えることで取得する地物をフィルタリングすることができました。


# おわりに
基本的な内容でしたが、体系的にdjango-rest-framework-gisを触ることができたので良い勉強になりました。