GeoDjangoでベクトルタイルを動的に取得してみる

:::note info
これは [MIERUNE AdventCalendar 2022](https://qiita.com/advent-calendar/2022/mierune)　9日目の記事です。
昨日は [@bordoray](https://qiita.com/bordoray) さんによる [星図を作ってみた⭐](https://qiita.com/bordoray/items/e1ee97fefb8090a7f877) でした。
:::



[MIERUNE](https://www.mierune.co.jp/)でバックエンドを担当しております[@selfsryo](https://github.com/selfsryo)といいます。
[去年のAdventCalendar](https://qiita.com/advent-calendar/2021/mierune)では、[GeoDjangoを触ってみよう！](https://qiita.com/selfsryo/items/3a56b8303827121e92cb)および[django-rest-framework-gisを触ってみよう](https://qiita.com/selfsryo/items/376758edcdb96895bc1a)というGeoDjango関連の記事を書きましたが、今回もGeoDjangoにまつわる記事を書きます。 

# 概要

## ベクトルタイル

GISに限ったことではありませんが、最近の動向としてGISデータの巨大化がますます進み、データを保持するのも配信するのも以前より難しくなってきている印象です。データフォーマットの軽量化 / 最適化のニーズが高まっていますが、その先駆け的存在であり、すでに枯れた技術の一つにベクトルタイルがあります。

このベクトルタイル、とても軽量で良いのですが、ベクトルを変換してしかるべき場所に格納して配信する、という手間があります。サイズが大きい場合、この作業にそれなりのマシンスペックが求められます。かつ静的ファイルであるので、データの更新が発生するたびに作成し直す必要があります。このように、ベクトルタイルの作成および管理には、少なくないコストが発生します。

## ST_AsMVTとGeoDjango

ベクトルタイルを動的に取得する方法として、DBから直接取得するというアプローチがあります。PostGISの場合、[ST_AsMVT](https://postgis.net/docs/ST_AsMVT.html)という空間関数が用意されており、これを実行することでDBから直接ベクトルタイルを取得することができます。

サーバーからGISデータを配信するとき、弊社ではGeoDjango + PostGISの組み合わせが鉄板となっています。このST_AsMTVをGeoDjangoから実行できればよさそうなのですが、GeoDjangoではST_AsMVTのORMは用意されておらず、かつ直接SQLを叩いて実行するのも大変・・。

と思っていた時に、便利そうなライブラリをみつけました。

https://github.com/corteva/djangorestframework-mvt

結構前からあるライブラリのようですが、全然気付かなかった・・。Django REST frameworkの拡張ライブラリで、裏側でST_AsMVTなどのSQLを実行してくれるようです。URLクエリから、動的にフィルタしてベクトルタイルを取得することができるようです。

超絶前置きが長くなりましたが、今回はこの[djangorestframework-mvt](https://github.com/corteva/djangorestframework-mvt)を触ってみた、というテーマの記事となります。

# ソースコード / データ

以下のリポジトリで試しました。[以前の記事](https://qiita.com/selfsryo/items/3a56b8303827121e92cb)で[GeoDjango公式チュートリアル](https://docs.djangoproject.com/en/4.0/ref/contrib/gis/tutorial/)を実施してみたのですが、その時のコードをインポートしたものです。

https://github.com/selfsryo/DRF-mvt-test

データはGeoDjangoの公式チュートリアルで使われている[国境データのShapefile](https://thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip)（クリックするとダウンロードされます）を使いました。

# 手順

## 準備

[公式ドキュメント](https://corteva.github.io/djangorestframework-mvt/html/index.html)に従って実行していきます。

まずはインストールします。pipenvを使っているので、以下を実行しました。

```
pipenv install djangorestframework-mvt
```

続いてモデルにManagerを追加します。MVTManagerの中では`geo_col`としてジオメトリのフィールド名を指定する必要があるので、`geo_col="mpoly"`とします。

```python
from django.contrib.gis.db import models
# 以下追加
from rest_framework_mvt.managers import MVTManager

class WorldBorder(models.Model):
    name = models.CharField(max_length=50)
    area = models.IntegerField()
    pop2005 = models.IntegerField('Population 2005')
    fips = models.CharField('FIPS Code', max_length=2, null=True)
    iso2 = models.CharField('2 Digit ISO', max_length=2)
    iso3 = models.CharField('3 Digit ISO', max_length=3)
    un = models.IntegerField('United Nations Code')
    region = models.IntegerField('Region Code')
    subregion = models.IntegerField('Sub-Region Code')
    lon = models.FloatField()
    lat = models.FloatField()

    mpoly = models.MultiPolygonField()

    # 以下2行追加
    objects = models.Manager()
    vector_tiles = MVTManager(geo_col="mpoly")

    def __str__(self):
        return self.name
```

最後に、urls.pyにパスを追加します。`mvt_view_factory`の中では、ジオメトリのフィールド名を`geom_col`として指定します。微妙な表記揺れが気になるところですが、なにか理由はあるのでしょうか。

```python
from django.contrib import admin
from django.urls import include, path
from rest_framework import routers
# 追加
from rest_framework_mvt.views import mvt_view_factory
from world.models import WorldBorder
from world.views import WorldBorderViewSet

router = routers.DefaultRouter()
router.register('border', WorldBorderViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('world/', include(router.urls)),
    # 追加
    path("api/mvt/", mvt_view_factory(WorldBorder, geom_col="mpoly")),
]
```

この状態でDockerを起動し、データをインサートします。これで準備完了です。

## QGISから表示確認

デスクトップGISアプリケーションであるQGISから配信を確認してみます。
今回はMac用QGISの以下のバージョンを使用しました。
`3.26.3-Buenos Aires`

QGISを起動し、`Vector Tiles`を右クリック > `新規一般接続`に遷移します。
![スクリーンショット 2022-12-08 21.50.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/ce8aaa9a-90b1-1647-fa04-39559b2dfef4.png)


URLには以下を設定します。
http://localhost:8000/api/mvt/?tile={z}/{x}/{y}
![スクリーンショット 2022-12-08 23.21.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/28c4e91a-6532-4043-0c4e-8dedc96a18f5.png)


このレイヤを表示させてみます。ちゃんと取得できていることがわかります。いい感じですね。
![スクリーンショット 2022-12-08 23.23.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/0a82668d-739d-e9b2-b6d3-f4915e85a004.png)



属性情報も確認できます。
![スクリーンショット 2022-12-08 23.25.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/560a4e36-953a-e34c-3e08-9532c6c1877d.png)


では、データの絞り込みを行ってみましょう。[公式ドキュメント](https://corteva.github.io/djangorestframework-mvt/html/index.html#getting-started)にある通り、URLクエリでカラム名を指定して取得するデータを絞ってみます。

今回は、日本のデータだけを取得します。モデルの中に`name`フィールドがあるので、`&name=Japan`をURLクエリに追加します。
http://localhost:8000/api/mvt/?tile={z}/{x}/{y}&name=Japan
![スクリーンショット 2022-12-08 23.40.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/019b60b9-ad81-0676-2e7f-b6eb1fe603ac.png)


表示できました。ちゃんと日本のデータだけ取得できているようです。
![スクリーンショット 2022-12-08 23.43.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/fba1c254-14d4-6a9f-73ef-e1e6e06dc418.png)


# 所感

今回は[djangorestframework-mvt](https://github.com/corteva/djangorestframework-mvt)を用いて、ベクトルタイルをGeoDjangoから動的に配信してみました。Djangoを使って動的にベクトルタイルを配信できるとなると、いろいろ使いどころがありそうです。

若干動作的にもっさりしている気がしないでもないですが、QGIS起因なのかどうなのか・・？パフォーマンス検証もしてみたいところです。

:::note info
明日は[@yuskesuzki@github](https://qiita.com/yuskesuzki@github)さんの記事です。乞うご期待！
:::