GeoDjangoを触ってみよう！

# はじめに

この記事は[MIERUNE Advent Calendar 2021](https://qiita.com/advent-calendar/2021/mierune) 2日目の記事です。今回はGISに絡んだ記事を書いてみたいと思っていたので、GeoDjangoを触ってみよう！というテーマで執筆させていただきました。



# GeoDjangoとは

Djangoが持つモジュールで、利用することでDjangoを通して地理空間データを扱うことができるようになります。Djangoそのものに含まれているモジュールです（[ソースコード](https://github.com/django/django/tree/main/django/contrib/gis)）。

[Django公式ドキュメントのトップページ](https://docs.djangoproject.com/ja/3.2/#geographic-framework)でも紹介されており、そこでは以下のように記載されています。

> [GeoDjango](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/) は世界規模の地理情報 Web フレームワークを目指しています。 GeoDjango の目的は、地理情報システム (GIS) のWeb アプリケーションの開発をより簡単にし、空間データ (spatially enabled data) を活用することにあります。

GeoDjangoでは[GDAL API](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/gdal/#module-django.contrib.gis.gdal)をサポートしており、Djangoを介して一部の[GDAL](https://gdal.org/)の機能を利用することができます。

# 前提

この記事では、[Django公式ドキュメントのGeoDjangoチュートリアル](https://docs.djangoproject.com/en/3.2/ref/contrib/gis/tutorial/)に則って、内容をかいつまんで進めてみます。

# 対象

- Djangoはある程度触ったことがある
- GeoDjangoははじめて



# バージョン

```
python_version = "3.8"
django = "==3.2.6"
psycopg2-binary = "==2.9.1"
```



# ソースコード/ 環境

以下に実行したソースコードを載せておきます。
https://github.com/selfsryo/GeoDjangoOfficialTutorial

GeoDjangoを扱うには当然Djangoのインストールと、地理空間用のDB（PostGIS等）が必要になります。今回はDockerでDjango + PostGISの環境を構築しました。GDALなどのGISに必要なライブラリもインストールしています。

以下のような構成になっています。

```
GeoDjangoOfficialTutorial
├── .gitignore
├── docker-compose.yml
├── geodjango
│   ├── .env.sample
│   ├── Dockerfile
│   ├── Pipfile
│   ├── 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
│       ├── tests.py
│       └── views.py
└── postgres
    ├── .env.db.sample
    ├── Dockerfile
    └── sql
        └── init.sql
```



# やってみよう

## 準備

まずは`geodjango`というDjangoのプロジェクトを作成します。Dockerを使っている場合はコンテナの中に入るなりして実行します。

```
$ django-admin startproject geodjango
```

その後、`world`というDjangoのアプリケーションを作成します。

```
$ cd geodjango
$ python3 manage.py startapp world
```

`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',
    'world',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'geodjango.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'geodjango.wsgi.application'

# 修正
DATABASES = {
    'default': {
        'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.contrib.gis.db.backends.postgis'),
        'NAME': os.environ.get('DATABASE_DB', 'postgres'),
        'USER': os.environ.get('DATABASE_USER', 'postgres'),
        'PASSWORD': os.environ.get('DATABASE_PASSWORD', 'password1234'),
        'HOST': os.environ.get('DATABASE_HOST', 'postgres'),
        'PORT': os.environ.get('DATABASE_PORT', '5432'),
    }
}


AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

# 修正
LANGUAGE_CODE = 'ja'

# 修正
TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

STATIC_URL = '/static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
```

その後今回使用する国境データを用意します。データは[こちら](https://thematicmapping.org/downloads/TM_WORLD_BORDERS-0.3.zip)（クリックするとzipがダウンロードされます）から入手することができます。先ほど作成したアプリケーションの`world`の中に`data`というフォルダを作り、その中で落としたzipを解凍します。中には`Sharpefile`が入っています（各ファイルの説明は[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/tutorial/#worldborders)参照）。解凍後、zipは削除してしまいましょう。ファイルの階層は以下のようになります。

```
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
├── tests.py
└── views.py
```





## ogrinfo

ここで、`ogrinfo`で国境データのShapefileの情報を覗いてみます。先ほど紹介したリポジトリからDockerを利用していると、コンテナの中で`ogrinfo`を用いることができます。

まずは以下を実行してメタデータを確認します。

```
$ ogrinfo world/data/TM_WORLD_BORDERS-0.3.shp
INFO: Open of `world/data/TM_WORLD_BORDERS-0.3.shp'
      using driver `ESRI Shapefile' successful.
1: TM_WORLD_BORDERS-0.3 (Polygon)
```

`TM_WORLD_BORDERS-0.3`というレイヤを持っていることがわかるので、詳細を出力してみます。レイヤ名を指定して`-so`をつけて実行します。

```
$ ogrinfo -so world/data/TM_WORLD_BORDERS-0.3.shp TM_WORLD_BORDERS-0.3
INFO: Open of `world/data/TM_WORLD_BORDERS-0.3.shp'
      using driver `ESRI Shapefile' successful.

Layer name: TM_WORLD_BORDERS-0.3
Metadata:
  DBF_DATE_LAST_UPDATE=2008-07-30
Geometry: Polygon
Feature Count: 246
Extent: (-180.000000, -90.000000) - (180.000000, 83.623596)
Layer SRS WKT:
GEOGCRS["WGS 84",
    DATUM["World Geodetic System 1984",
        ELLIPSOID["WGS 84",6378137,298.257223563,
            LENGTHUNIT["metre",1]]],
    PRIMEM["Greenwich",0,
        ANGLEUNIT["degree",0.0174532925199433]],
    CS[ellipsoidal,2],
        AXIS["latitude",north,
            ORDER[1],
            ANGLEUNIT["degree",0.0174532925199433]],
        AXIS["longitude",east,
            ORDER[2],
            ANGLEUNIT["degree",0.0174532925199433]],
    ID["EPSG",4326]]
Data axis to CRS axis mapping: 2,1
FIPS: String (2.0)
ISO2: String (2.0)
ISO3: String (3.0)
UN: Integer (3.0)
NAME: String (50.0)
AREA: Integer (7.0)
POP2005: Integer64 (10.0)
REGION: Integer (3.0)
SUBREGION: Integer (3.0)
LON: Real (8.3)
LAT: Real (7.3)
```

これで、レイヤ内の地物数（246）、空間参照系、および属性のタイプ等が確認できます。



## モデルの定義

上の属性に合わせて、`models.py`を以下のように定義します。modelsは`django.db.models`ではなく、`django.contrib.gis.db.models`からimportします。

```python
from django.contrib.gis.db import models

class WorldBorder(models.Model):
    # Regular Django fields corresponding to the attributes in the
    # world borders shapefile.
    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()

    # GeoDjango-specific: a geometry field (MultiPolygonField)
    mpoly = models.MultiPolygonField()

    # Returns the string representation of the model.
    def __str__(self):
        return self.name
```

mpoly(`MultiPolygonField`)には何も指定していませんが、空間参照系はデフォルトで`srid=4326`となっています（[参照](https://github.com/django/django/blob/stable/3.2.x/django/contrib/gis/db/models/fields.py#L67)）。他の空間参照系を指定したい場合は引数に`srid`で指定します。

models.pyを定義したら、以下のコマンドでマイグレーションを実行します。

```
$ python3 manage.py makemigrations
$ python3 manage.py migrate
```



## インポート実行

先ほどダウンロードした国境データのShapefileをDBにインポートする方法はいくつかありますが、今回はGeoDjangoに備わっている`LayerMapping`を利用してみます（こちら[参照](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/layermapping/)）。

後ほどスクリプトで実行しますが、いったんDjango Shellを用いつつ手順を追っていきます。

```
$ python3 manage.py shell
```

先ほどダウンロードした国境データのShapefileをPythonのPathオブジェクトにします。

```
>>> from pathlib import Path
>>> import world
>>> world_shp = Path(world.__file__).resolve().parent / 'data' / 'TM_WORLD_BORDERS-0.3.shp'
```

続いて、`django.contrib.gis.gdal.DataSource`（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/gdal/#django.contrib.gis.gdal.DataSource)）を利用して、国境データのShapefileを開きます。

```
>>> from django.contrib.gis.gdal import DataSource
>>> ds = DataSource(world_shp)
>>> ds
<django.contrib.gis.gdal.datasource.DataSource object at 0x7f0a6df717c0>
>>> print(ds)
/usr/src/app/world/data/TM_WORLD_BORDERS-0.3.shp (ESRI Shapefile)
```

レイヤを開いてみます。`Shapefile`は1つのレイヤしか持てませんが、確認してみると先ほど`ogrinfo`で確認できた`TM_WORLD_BORDERS-0.3`が表示されます。以下の`lyr`は`Layer`オブジェクト（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/gdal/#django.contrib.gis.gdal.Layer)）です。

```
>>> print(len(ds))
>>> lyr = ds[0]
>>> lyr
<django.contrib.gis.gdal.layer.Layer object at 0x7f0a6df71700>
>>> print(lyr)
TM_WORLD_BORDERS-0.3
```

レイヤからジオメトリタイプや地物数、属性フィールドも確認できます。

```
>>> print(lyr.geom_type)
Polygon
>>> len(lyr)
246
>>> print(lyr.fields)
['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT']
```

この[Layer](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/gdal/#layer)オブジェクトに空間参照系が関連づいていた場合、`srs`という属性を持ちます。これは`SpatialRegerence`というオブジェクト（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/gdal/#django.contrib.gis.gdal.SpatialReference)）として定義されています。また`proj`属性を確認することで、測地系が`WGS84`だと判別できます。

```
>>> srs = lyr.srs
>>> srs
<django.contrib.gis.gdal.srs.SpatialReference object at 0x7f9b6bd8c670>
>>> print(srs)
GEOGCS["WGS 84",
    DATUM["WGS_1984",
        SPHEROID["WGS 84",6378137,298.257223563,
            AUTHORITY["EPSG","7030"]],
        AUTHORITY["EPSG","6326"]],
    PRIMEM["Greenwich",0,
        AUTHORITY["EPSG","8901"]],
    UNIT["degree",0.0174532925199433,
        AUTHORITY["EPSG","9122"]],
    AXIS["Latitude",NORTH],
    AXIS["Longitude",EAST],
    AUTHORITY["EPSG","4326"]]
>>> srs.proj
'+proj=longlat +datum=WGS84 +no_defs'
```

レイヤから任意の地物を取得してみます。これは`Feature`オブジェクト（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/gdal/#feature)）として取得することができます。

```
>>> feat = lyr[234]
>>> feat
<django.contrib.gis.gdal.feature.Feature object at 0x7f9b6bd14970>
>>> print(feat.get('NAME'))
San Marino
```

`Feature`からはジオメトリを取得でき、WKTおよびGeoJSONとして出力することが可能です。

```
>>> geom = feat.geom
>>> print(geom.wkt)
POLYGON ((12.415798 43.957954,12.450554 43.979721,12.453888 43.981667,12.4625 43.984718,12.471666 43.986938,12.492777 43.989166,12.505554 43.988609,12.509998 43.986938,12.510277 43.982773,12.511665 43.943329,12.510555 43.939163,12.496387 43.923332,12.494999 43.914719,12.487778 43.90583,12.474443 43.897217,12.464722 43.895554,12.459166 43.896111,12.416388 43.904716,12.412222 43.906105,12.407822 43.913658,12.403889 43.926666,12.404999 43.948326,12.408888 43.954994,12.415798 43.957954))
>>> print(geom.json)
{ "type": "Polygon", "coordinates": [ [ [ 12.415798, 43.957954 ], [ 12.450554, 43.979721 ], [ 12.453888, 43.981667 ], [ 12.4625, 43.984718 ], [ 12.471666, 43.986938 ], [ 12.492777, 43.989166 ], [ 12.505554, 43.988609 ], [ 12.509998, 43.986938 ], [ 12.510277, 43.982773 ], [ 12.511665, 43.943329 ], [ 12.510555, 43.939163 ], [ 12.496387, 43.923332 ], [ 12.494999, 43.914719 ], [ 12.487778, 43.90583 ], [ 12.474443, 43.897217 ], [ 12.464722, 43.895554 ], [ 12.459166, 43.896111 ], [ 12.416388, 43.904716 ], [ 12.412222, 43.906105 ], [ 12.407822, 43.913658 ], [ 12.403889, 43.926666 ], [ 12.404999, 43.948326 ], [ 12.408888, 43.954994 ], [ 12.415798, 43.957954 ] ] ] }
```



---

少し長くなりましたが、上でやったことを踏まえ、国境データのShapefileをDBにインポートするようなスクリプトを書いてみます。`world`以下に`load.py`というモジュールを作成し、以下のようにします。

```python
from pathlib import Path
from django.contrib.gis.utils import LayerMapping
from .models import WorldBorder

world_mapping = {
    'fips' : 'FIPS',
    'iso2' : 'ISO2',
    'iso3' : 'ISO3',
    'un' : 'UN',
    'name' : 'NAME',
    'area' : 'AREA',
    'pop2005' : 'POP2005',
    'region' : 'REGION',
    'subregion' : 'SUBREGION',
    'lon' : 'LON',
    'lat' : 'LAT',
    'mpoly' : 'MULTIPOLYGON',
}

world_shp = Path(__file__).resolve().parent / 'data' / 'TM_WORLD_BORDERS-0.3.shp'

def run(verbose=True):
    lm = LayerMapping(WorldBorder, world_shp, world_mapping, transform=False)
    lm.save(strict=True, verbose=verbose)
```

`world_mapping`という辞書では、キーに`WorldBorder`モデルのフィールド、値に国境データのShapefileの持つ属性がマップされています。また今回は座標系を移動させる必要がないため、`transform=False`とします。

このスクリプトを実行してみます。先ほどと同じくDjango Shellを実行します。

```
$ python3 manage.py shell
```

続いて、以下のコマンドを叩きます。

```
>>> from world import load
>>> load.run()
```

実行すると`Saved:国の名前`と出力されるはずです。`WorldBorder`モデルの中で`__str__()`を定義しているため、name属性が表示されるようになっています。（`__str__()`をコメントアウトすると`WorldBorder object (〇〇)`と出力されます）

```
Saved: Antigua and Barbuda
Saved: Algeria
Saved: Azerbaijan
Saved: Albania
Saved: Armenia
Saved: Angola
Saved: American Samoa
Saved: Argentina
Saved: Australia
・・・・
```



## ogrinspect

Djangoには、`ispectdb`という便利なコマンドがあり、これを実行するとDBのmodesl.pyを自動で出力してくれます。GeoDjangoではベクターデータに対応したコマンドとして、`ogrinspect`というコマンドが用意されています（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/commands/#django-admin-ogrinspect)）。

今回の場合だと、以下のようなコマンドを実行します。

```
$ python3 manage.py ogrinspect world/data/TM_WORLD_BORDERS-0.3.shp WorldBorder --srid=4326 --mapping --multi
```

結果、国境データのShapefileからそれに合ったmodels.pyの結果を出力してくれます。

```
from django.contrib.gis.db import models


class WorldBorder(models.Model):
    fips = models.CharField(max_length=2)
    iso2 = models.CharField(max_length=2)
    iso3 = models.CharField(max_length=3)
    un = models.IntegerField()
    name = models.CharField(max_length=50)
    area = models.IntegerField()
    pop2005 = models.BigIntegerField()
    region = models.IntegerField()
    subregion = models.IntegerField()
    lon = models.FloatField()
    lat = models.FloatField()
    geom = models.MultiPolygonField(srid=4326)


# Auto-generated `LayerMapping` dictionary for WorldBorder model
worldborder_mapping = {
    'fips': 'FIPS',
    'iso2': 'ISO2',
    'iso3': 'ISO3',
    'un': 'UN',
    'name': 'NAME',
    'area': 'AREA',
    'pop2005': 'POP2005',
    'region': 'REGION',
    'subregion': 'SUBREGION',
    'lon': 'LON',
    'lat': 'LAT',
    'geom': 'MULTIPOLYGON',
}
```

なお、このコマンドは`settings.py`の`INSTALLED_APPS`に`'django.contrib.gis'`を含めておかないと実行できないので注意が必要です。



## クエリセットAPI

DjangoのORMには多彩なコマンド（クエリセットAPIと呼ばれています。こちら[参照](https://docs.djangoproject.com/en/3.2/ref/models/querysets/)）が用意されています。またこの中で、SQLのWHERE句に相当する機能として[lookups](https://docs.djangoproject.com/en/3.2/ref/models/querysets/#field-lookups)と呼ばれる構文を指定することができ、これらを利用することでSQLを叩かずとも欲しいクエリセットを取得することができます。

そしてGeoDjangoでは、通常とは別に、GISとしての利用に特化した多彩なlookupsが用意されています（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geoquerysets/#)参照）。試しに実行してみます。再度Django Shellを起動します。

```
$ python3 manage.py shell
```

起動したら、WKT形式（経度 緯度）で富士山の`POINT`を定義します。

```
>>> pnt_wkt = 'POINT(138.729952 35.359455)'
```

この`POINT`を含んでいるインスタンスのクエリセットを取得してみます。`contains`というlookups（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geoquerysets/#contains)）を指定します。通常のDjangoでも[contains](https://docs.djangoproject.com/en/3.2/ref/models/querysets/#contains)というlookupsは存在しますが、GeoDjangoではその地点を含むクエリセットを返してくれます。

```
>>> from world.models import WorldBorder
>>> WorldBorder.objects.filter(mpoly__contains=pnt_wkt)
<QuerySet [<WorldBorder: Japan>]>
```

WKT形式ではなく、`GEOS`オブジェクト（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geos/#module-django.contrib.gis.geos)）としてもクエリセットを取得できます。同じく富士山のPointを定義します。

```
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(138.729952,35.359455)
```

次は`intersects`というlookups（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geoquerysets/#intersects)）を利用してみます。getと組み合わせて、空間的に交差するインスタンスを取得することができます。

```
>>> WorldBorder.objects.get(mpoly__intersects=pnt)
<WorldBorder: Japan>
```

最後に富士山から1,000km以内に存在するインスタンスのクエリセットを取得してみます。`distance_lt`（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geoquerysets/#distance-lt)）というlookupsを用いて、距離を基準にフィルタすることができます。

```
>>> from django.contrib.gis.measure import D
>>> WorldBorder.objects.filter(mpoly__distance_lt=(pnt_wkt, D(km=1000)))
<QuerySet [<WorldBorder: Korea, Republic of>, <WorldBorder: Japan>, <WorldBorder: Korea, Democratic People's Republic of>, <WorldBorder: Russia>]>
```

このように、GeoDjangoには[とても便利なクエリセットAPI](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geoquerysets/#gis-queryset-api-reference)がたくさん用意されています。



## 座標参照系の自動変換

GeoDjangoでクエリを実行する際、座標参照系の変換も自動的に行ってくれます。`srid=3857`に指定して、先ほどと同じ富士山の座標を定義します。

```
>>> from world.models import WorldBorder
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(15443347.614415286, 4212837.5728517035, srid=3857)
```

この座標を利用して、`srid=4326`で登録されている今のモデルに対してクエリを叩きます。先ほどと同じ結果が返ってきます。

```
>>> from django.contrib.gis.measure import D
>>> WorldBorder.objects.filter(mpoly__distance_lt=(pnt, D(km=1000)))
<QuerySet [<WorldBorder: Korea, Republic of>, <WorldBorder: Japan>, <WorldBorder: Korea, Democratic People's Republic of>, <WorldBorder: Russia>]>
```

内部的には以下のようなSQLが実行されています。DjangoのORMが裏側で変換をしてくれているのがわかります。

```
>>> qs = WorldBorder.objects.filter(mpoly__distance_lt=(pnt, D(km=1000)))
>>> print(qs.query)
SELECT "world_worldborder"."id", "world_worldborder"."name", "world_worldborder"."area", "world_worldborder"."pop2005", "world_worldborder"."fips", "world_worldborder"."iso2", "world_worldborder"."iso3", "world_worldborder"."un", "world_worldborder"."region", "world_worldborder"."subregion", "world_worldborder"."lon", "world_worldborder"."lat", "world_worldborder"."mpoly"::bytea FROM "world_worldborder" WHERE ST_DistanceSphere("world_worldborder"."mpoly", ST_Transform(ST_GeomFromEWKB('\001\001\000\000 \021\017\000\000?J\251s\262tmA1\232\251d\031\022PA'::bytea), 4326)) < 1000000.0
```



## GeoDjangoにおける遅延評価

Djangoではクエリの実行において、実際に値が必要になるまでクエリを叩かない、という最適化手法を取っています。これを遅延評価といいます（[参考](https://docs.djangoproject.com/ja/3.2/topics/db/queries/#querysets-are-lazy)）。それとは若干異なるのですが、GeoDjangoでは実際にインスタンスのジオメトリフィールドにアクセスした際、初めてオブジェクトが作成されるような仕様になっています。

以下では`jp`というインスタンスを取得しています。この際、GeoDjangoは`GEOSGeometry`オブジェクト（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/geos/#django.contrib.gis.geos.GEOSGeometry)）を作成します。

```
>>> jp = WorldBorder.objects.get(name='Japan')
>>> jp.mpoly
<MultiPolygon object at 0x7f23279cdd90>
```

このジオメトリオブジェクトは、様々な地理空間的属性およびメソッドを兼ね備えています。

```
>>> jp.mpoly.wkt
'MULTIPOLYGON (((153.958588 24.294998, 153.953308 24.292774, 153.946625 24.293331, 153.942749 24.296944, 153.939697 24.300831,
・・・・・
>>> jp.mpoly.geojson
'{ "type": "MultiPolygon", "coordinates": [ [ [ [ 153.958588, 24.294998 ], [ 153.953308, 24.292774 ], [ 153.946625, 24.293331 ], [ 153.942749, 24.296944 ], [ 153.939697, 24.300831 ],
・・・・・
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(138.729952,35.359455)
>>> jp.mpoly.contains(pnt)
True
```



## アドミンページ

Djangoの非常に強力な機能の一つにアドミンページがあります。GeoDjangoでは、このアドミンページに関しても、地理空間的に特化した機能が搭載されています。

さっそく試してみましょう。`world`の中の`admin.py`を以下のようにします。`django.contrib.admin`ではなく、`django.contrib.gis.admin`（[こちら](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/admin/#django.contrib.gis.admin.GeoModelAdmin)）をインポートするのがポイントです。

```python
from django.contrib.gis import admin
from .models import WorldBorder

admin.site.register(WorldBorder, admin.GeoModelAdmin)
```

あとは以下のコマンドでアドミンユーザーを作成します。

```
$ python3 manage.py createsuperuser
```

実行が終わったら`http://localhost:8000/admin/`にアクセスします。ユーザー情報を入力してログインし、`WorldBorder`の任意のオブジェクト編集画面に遷移します。以下のように、Djangoのアドミンページから国境を編集することができます。

![test.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1310757/973f34e1-6b0a-7d14-62c0-f9fe8f5c4e7f.png)


その他ライブラリを導入することで背景マップを変更ができたりします。アドミンページにも様々な機能が実装されています。

# おわりに

改めてGeoDjangoを触ってみることで、まだまだたくさんの強力な機能があることに気付けました。今後もGeoDjangoの素晴らしさを紹介していくことができたらと思います。





# 参考

[Django公式ドキュメントのGeoDjangoのページ](https://docs.djangoproject.com/ja/3.2/ref/contrib/gis/)

[GeoDjangoではじめる地理空間情報](https://homata.gitbook.io/geodjango/)