🌏

Django (GeoDjango, Docker)で現在地に近い店舗検索できるようになるまでやったこと

2022/05/13に公開

最終的なゴールとしてはこんな感じの緯度経度を持ったモデルを作り

from django.contrib.gis.db import models

class Omise(models.Model):
	location = models.PointField(null=True, blank=True, srid=4326, verbose_name='Location')

こんな感じでユーザから送ってもらう緯度経度を元に近いものを返すところをゴールとします。

res = (
        Omise.objects.filter(location__distance_lte=(ref_location, distance))
        .annotate(distance=Distance("location", ref_location))
        .order_by("distance")
)

動作確認したバージョン

python: 3.9.12
Django: 4.0.4
GDAL: 3.2.2

GDAL を導入する

まず地点のデータを保存できるようにするために GDAL というものをインストールする必要があります。

今回は Docker を使っていたので次のように gdal 関連のもののインストールの記述を追加しました。

RUN apt-get update && apt-get install -y --no-install-recommends \
     tzdata gdal-bin libgdal-dev

ARG CPLUS_INCLUDE_PATH=/usr/include/gdal
ARG C_INCLUDE_PATH=/usr/include/gdal

次に pip でインストールをしますが、この時 setuptools のバージョンを下げないと

Complete output (5 lines):
    /usr/lib/python3.7/distutils/dist.py:274: UserWarning: Unknown distribution option: 'use_2to3_fixers'
      warnings.warn(msg)
    /usr/lib/python3.7/distutils/dist.py:274: UserWarning: Unknown distribution option: 'use_2to3_exclude_fixers'
      warnings.warn(msg)
    error in GDAL setup command: use_2to3 is invalid.

みたいな感じのエラーが出て途方に暮れます。なのでバージョンを下げてインストールします。
また、上記でインストールした GDAL のバージョンと pip でインストールするもののバージョンを揃える必要があるため GDAL==$(gdal-config --version) という風にバージョンを指定してあげます。

# setuptools のバージョンを下げないと GDAL のインストールが失敗する
# https://qiita.com/nkmr_RL/items/85edc2ee68c01ec5582e
RUN pip install setuptools==57.4.0
RUN pip install GDAL==$(gdal-config --version) --global-option=build_ext --global-option="-I/usr/include/gdal"

Django の設定

次に settings.py の INSTALLED_APPS に GIS を加えます。

INSTALLED_APPS = [
    ...
    "django.contrib.gis",
]

また、データエースのエンジンも変えておきます。
下記は MySQL の例ですが、ここは使っている DB に応じて適宜変えてください。

DATABASES = {
    "default": {
        "ENGINE": 'django.contrib.gis.db.backends.mysql', 
	...
    }
}

モデルに地理情報を追加

PointField という緯度経度を Tuple で保存できるものでカラムを作ります。

from django.contrib.gis.db import models

class Omise(models.Model):
	location = models.PointField(null=True, blank=True, srid=4326, verbose_name='Location')

ちなみに SRID というのは空間参照情報のことを指します(よく分かっていない)
https://www.jurigis.me/2015/02/05/about-srid/

実際にクエリを実行

最後にこんな感じで現在地の検索を行います。
お疲れ様でした。

from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.measure import D

# メートルを degree unit に変換する関数
# 参考: https://gis.stackexchange.com/questions/94645/geodjango-error-only-numeric-values-of-degree-units-are-allowed-on-geographic-d
def distance_to_decimal_degrees(distance, latitude):
    lat_radians = latitude * (math.pi / 180)
    # 1 longitudinal degree at the equator equal 111,319.5m equiv to 111.32km
    return math.fabs(distance.m / (111_319.5 * math.cos(lat_radians)))

distance = distance_to_decimal_degrees(D(m=1000), latitude)
ref_location = Point(latitude, longitude)
res = (
        Omise.objects.filter(location__distance_lte=(ref_location, distance))
        .annotate(distance=Distance("location", ref_location))
        .order_by("distance")
)

参考になったページ

https://stackoverflow.com/questions/43818498/object-has-no-attribute-distance-geodjango

https://qiita.com/StrayDog/items/0aac7e11051cbf1c8e26
https://github.com/OSGeo/gdal/issues/2827

https://qiita.com/StrayDog/items/0aac7e11051cbf1c8e26

Discussion