DjangoとMySQLで緯度経度を扱う

やりたかったことは、指定した緯度経度(GPSから取得した現在位置)に近い場所の検索と距離の算出でした。
DjangoだとPostGIS+GeoDjangoを使えばできますが、今回はデータベースがMySQLであるため、filterでdistanceが使えませんでした。
GeoDjango Database API | Django documentation | Django
いい方法がないかググっていたら、Google Developersサイト内のGoogle Maps APIに関する記事で、PHPMySQLを使う例があり、参考になりました。
Creating a Store Locator with PHP, MySQL & Google Maps  |  Google Maps APIs  |  Google Developers
このページにあるSQLを少しいじってDjangoで使う例を紹介します。

モデルの作成

Google Developersの例に習って店舗のモデル(店舗名, 緯度, 経度)を作ってみます。

myapp/models.py
# coding: utf-8
from django.db import models

class Shop(models.Model):
    name = models.CharField(max_length=255)
    latitude = models.DecimalField(u'緯度', max_digits=9, decimal_places=6, default=0)
    longitude = models.DecimalField(u'経度', max_digits=9, decimal_places=6, default=0)

    class Meta:
        db_table = 'shop'

    def __unicode__(self):
        return self.name

データ投入

店舗ではないけど、適当な場所の位置情報を登録します。

$ python manage.py shell
>>> from myapp.models import Shop
>>> Shop.objects.create(name=u'株式会社ビープラウド', latitude=35.6802361, longitude=139.70130849999998)
<Shop: 株式会社ビープラウド>
>>> Shop.objects.create(name=u'JR代々木駅', latitude=35.6830905,  longitude=139.70222620000004)
<Shop: JR代々木駅>
>>> Shop.objects.create(name=u'北参道駅', latitude=35.678156, longitude=139.70567800000003)
<Shop: 北参道駅>
>>> Shop.objects.create(name=u'JR原宿駅', latitude=35.670168, longitude=139.70268699999997)
<Shop: JR原宿駅>

指定した緯度経度から近い場所を検索する

指定した緯度経度から近い場所を取得する関数を作成します。

myapp/api.py
from myapp.models import Shop

def get_shop_list(lat, lng):
    shop_queryset = Shop.objects.raw("""
SELECT
  *,
  (
    6371
    * acos(
      cos(radians(%%s))
      * cos(radians(latitude))
      * cos(radians(longitude) - radians(%%s))
      + sin(radians(%%s))
      * sin(radians(latitude)) 
    ) 
  ) AS distance 
FROM %(table)s 
HAVING distance < 1 
ORDER BY distance
""" % {'table': Shop._meta.db_table},
        [lat, lng, lat])
    return shop_queryset

Google Developersのページに書かれているSQLを参考にし、rawメソッドでQuerySetを取得しています。
"HAVING distance < 1" で1km以内の結果だけを返すようにしています。

実行結果

作成した関数を実行してみます。distanceの値はキロメートル単位なので、1000倍してメートル単位で表示しています。

>>> from myapp.api import get_shop_list
>>> for shop in get_shop_list(35.6830905, 139.70222620000004):  # 代々木駅の緯度経度
...     print shop.name, shop.latitude, shop.longitude, "%.1fm" % (shop.distance * 1000)
...     
JR代々木駅 35.683090 139.702226 0.0m
株式会社ビープラウド 35.680236 139.701308 328.1m
北参道駅 35.678156 139.705678 631.1m

代々木駅から1km以内の場所を近い順に表示できました。
ここで紹介したコードはbitbucketに置いてます。
tokibito / sample_nullpobug / source / django / geo_mysql_example — Bitbucket