自動化無しに生活無し

WEBとかAIとかLinux関係をひたすら書く備忘録系ブログ

DjangoでOpenStreetMap(OSM)とleaflet.jsを使ってマッピングアプリを作る

thumbnail

※この方法はDjangoでなくても実現できる。

Djangoでマッピングを実現する方法としてGeoDjangoがある。だが、GeoDjangoは実装が容易ではなく、以前紹介した方法では実現できない事がわかった。

そこで、GeoDjangoよりも容易にマッピングを実現するため、オープンストリートマップ(以下、OSM)とleaflet.jsを使って対処する。

ソースコードは40分Djangoをベースとしている。

OSMとLeaflet.jsを実装させる

以下、実装手順を解説する。

緯度と経度を記録するモデルを作る

from django.db import models

class Topic(models.Model):

    comment     = models.CharField(verbose_name="コメント",max_length=2000)
    lat         = models.DecimalField(verbose_name="緯度",max_digits=9, decimal_places=6)
    lon         = models.DecimalField(verbose_name="経度",max_digits=9, decimal_places=6)

    def __str__(self):
        return self.comment

緯度と経度はデータベースへ負担をかけないようDecimalFieldを使ったが、JavaScript上で小数点以下の処理が煩雑になるので、FloatFieldでも良いだろう。

FloatFieldのmax_digitsは最大桁数、decimal_placesは小数部の値を意味している。

latは緯度を意味するlatitudelonは経度を意味するlongitudeからきている。

これをマイグレーションする。

フォームクラスを作り、ビューでバリデーションをする

モデルを継承したフォームクラスを作り、ビュー側でそれを使ってバリデーションする。

まず、forms.py

from django import  forms
from .models import Topic

class TopicForm(forms.ModelForm):

    class Meta:
        model   = Topic
        fields  = [ "comment","lat","lon" ]

続いてviews.py

from django.shortcuts import render,redirect
from django.views import View

from .models import Topic
from .forms import TopicForm

class IndexView(View):

    def get(self, request, *args, **kwargs):

        topics  = Topic.objects.all()
        context = { "topics":topics }

        return render(request,"bbs/index.html",context)

    def post(self, request, *args, **kwargs):

        form    = TopicForm(request.POST)

        if form.is_valid():
            print("OK")
            form.save()

        return redirect("bbs:index")

index   = IndexView.as_view()

これは以前解説した、モデルを継承したフォームクラスを使ったバリデーションと全く同じである。

フロント側でJavaScriptを発動させる

templates/bbs/index.htmlを下記のようにする。

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
	<title>コメント付きマップ</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>

    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>

<script>
    const topics = [
        {% for topic in topics %}
        { lat:{{ topic.lat }}, lon:{{ topic.lon }}, comment:"{{ topic.comment }}" },
        {% endfor %}
    ];
</script>
<style>
#map {
    height:90vh;
}
</style>

</head>
<body>

    <h1 class="bg-success text-white text-center">コメント付きマップ</h1>

    <main>
        
        <div class="row mx-0">
        
            <div class="col-sm-6">
                <div id="map"></div>
            </div>
            <div class="col-sm-6">
                <form method="POST">
                    {% csrf_token %}
                    <input id="lat_input" type="text" name="lat" placeholder="緯度" required maxlength=9>
                    <input id="lon_input" type="text" name="lon" placeholder="経度" required maxlength=9>
                    <textarea class="form-control" name="comment"></textarea>
                    <input type="submit" value="送信">
                </form>
                {% for topic in topics %}
                <div class="border">
                    <div>{{ topic.comment }}</div>
                </div>
                {% endfor %}
            </div>
        </div>

    </main>

    <script>
        //マップの表示位置を指定(緯度・経度)
        MAP = L.map('map').setView([34.6217684, -227.2109985], 9);
        MARKER = null;

        //地図データはOSMから読み込み
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        }).addTo(MAP);

        for (let topic of topics ){
            L.marker([topic["lat"], topic["lon"]]).addTo(MAP).bindPopup(topic["comment"]).openPopup();
        }

        //マウスクリックで緯度と経度の取得とポイント設置
        function map_click(e) {

            if (MARKER){
                MAP.removeLayer(MARKER);
            }

            MARKER = L.marker(e.latlng).addTo(MAP);
            console.log(e.latlng);

            $("#lat_input").val(Math.round(e.latlng["lat"]*1000000)/1000000);
            $("#lon_input").val(Math.round(e.latlng["lng"]*1000000)/1000000);
        }
        MAP.on('click', map_click);
    </script>
</body>
</html>

実践では、JavaScriptはHTMLに直接書かず、staticディレクトリに配置すると良いだろう。CDNも同様にDLしてstaticディレクトリに配置したほうが良い。

マップをクリックするとクリックした位置に、マーカーが表示される。下記記事を元に古いマーカーをif文で分岐して存在すれば削除するようにしている。

【Leaflet.js】地図をクリックしてマーカーを配置した時、古いマーカーを削除する

動かすとこうなる。

後は地図の検索機能などを入れるとより使いやすくなるだろう。

観光地や飲食店のレビューであれば、モデルに画像フィールドを追加すればよい。

結論

思っていたよりも簡単にマッピング系ウェブアプリが実現できた。

後は地図の見た目や挙動を工夫する必要がある。英語ではあるが、leaflet.jsのドキュメントを読むと良いだろう。

https://leafletjs.com/reference.html

指定した領域を囲んだりすることも、マーカーのデザインを変えることもできるようだ。

【Leaflet.js】オリジナルのアイコン画像を使用して、地図上に表示させる【飲食店のマッピングであれば食べ物の画像を使って視認性UP】

スポンサーリンク

シェアボタン

Twitter LINEで送る Facebook はてなブログ