GeoDjangoのチュートリアル

PostGISに入力したデータを使った処理を作成して行きたいと思います。

RESTful APIの作成

leaflet.js等のJavaScriptとサーバとのインターフェースとしてRESTful APIを実装します。 DjangoでRESTful APIを実装する為に、Django REST framework(DRF)を使用します。 django-rest-framework-gisは、このDRFに地理空間機能拡張したモジュールです。

pipコマンドを使って追加をします。

  • djangorestframework-gis : RESTful APIモジュール Django REST framework(DRF)の地理空間機能拡張バージョン

  • django-filter : 検索機能モジュール

  • markdown : Markdown変換ライブラリ

(env) $ pip install djangorestframework-gis
(env) $ pip install django-filter   # Filtering support
(env) $ pip install markdown        # Markdown support for the browsable API.
(env) $ pip freeze
  :
djangorestframework==3.8.2
djangorestframework-gis==0.13
django-filter==1.1.0
Markdown==2.6.11

Note

編集対象ファイル

├── geodjango
│   ├── settings.py       <-- 設定
│   └── urls.py           <-- REST APIのURL設定
└── world
    ├── serializers.py   <-- REST APIで使うシリアライザー
    └── views.py          <-- REST APIのビュー

geodjango/settings.py設定

インストールしたアプリケーションを設定ファイルのsettings.pyに追加します

(env) $ vi geodjango/settings.py
INSTALLED_APPS = [
    :
    'django_filters', 
    'rest_framework',
    'rest_framework_gis',
    'markdown', 
]

world/serializers.py作成

シリアライザはデータベースとAPIのとの間でデータフォーマットの変換をします。 worldアプリにserializers.pyファイルを作成します。

(env) $ vi world/serializers.py
from rest_framework import serializers
from .models import Border, School, Facility, Busstop

class BorderSerializer(serializers.ModelSerializer):
    class Meta:
        model = Border
        fields = ('__all__')

class SchoolSerializer(serializers.ModelSerializer):
    class Meta:
        model = School
        fields = ('__all__')

class FacilitySerializer(serializers.ModelSerializer):
    class Meta:
        model = Facility
        fields = ('__all__')

class BusstopSerializer(serializers.ModelSerializer):
    class Meta:
        model = Busstop
        exclude = ("p11_003_2", "p11_003_3", "p11_003_4", "p11_003_5", "p11_003_6", "p11_003_7", "p11_003_8", "p11_003_9",
                   "p11_003_10", "p11_003_11", "p11_003_12", "p11_003_13", "p11_003_14", "p11_003_15",
                   "p11_003_16", "p11_003_17", "p11_003_18", "p11_003_19",
                   "p11_004_2", "p11_004_3", "p11_004_4", "p11_004_5", "p11_004_6", "p11_004_7", "p11_004_8", "p11_004_9",
                   "p11_004_10", "p11_004_11", "p11_004_12", "p11_004_13", "p11_004_14", "p11_004_15", "p11_004_16",
                   "p11_004_17", "p11_004_18", "p11_004_19")

world/views.py作成

ビューでリクエストに対するレスポンスの設定をします。

(env) $ vi world/views.py
from rest_framework import viewsets
from rest_framework_gis.filters import DistanceToPointFilter, InBBoxFilter
from rest_framework.pagination import PageNumberPagination

from .serializers import BorderSerializer, SchoolSerializer, FacilitySerializer, BusstopSerializer
from .models import Border, School, Facility, Busstop

class MyPagination(PageNumberPagination):
    page_size_query_param = 'page_size'

class BorderViewSet(viewsets.ModelViewSet):
    queryset = Border.objects.all()
    serializer_class = BorderSerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter,)
    distance_filter_field = 'geom'
    distance_filter_convert_meters = True

class SchoolViewSet(viewsets.ModelViewSet):
    queryset = School.objects.all()
    serializer_class = SchoolSerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter,)
    distance_filter_field = 'geom'
    distance_filter_convert_meters = True

class FacilityViewSet(viewsets.ModelViewSet):
    queryset = Facility.objects.all()
    serializer_class = FacilitySerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter,)
    distance_filter_field = 'geom'
    distance_filter_convert_meters = False

class BusstopViewSet(viewsets.ModelViewSet):
    queryset = Busstop.objects.all()
    serializer_class = BusstopSerializer
    pagination_class = MyPagination
    filter_backends = (DistanceToPointFilter, InBBoxFilter)
    distance_filter_field = bbox_filter_field = 'geom'
    distance_filter_convert_meters = True

設定項目

  • queryset: クエリーデータ一覧

  • serializer_class: シリアライズ・デシリアライズで使用するserializer_classを指定

  • pagination_class: ページングの設定

  • filter_backends: データを絞り込む方法を設定

    • DistanceToPointFilter: 指定した点からの距離で絞り込むフィルタ

    • InBBoxFilter: バウンダリでの絞り込むフィルタ。南西端、北東端の経度、緯度を指定する

  • distance_filter_field: フィルタの対象フィールドを設定

  • bbox_filter_field: フィルタの対象フィールドを設定

  • distance_filter_convert_meters: 距離でのフィルター (??? 多分。。。。)

django-rest-framework-gisのフィルター

  • InBBOXFilter

  • GeometryFilter

  • GeoFilterSet

  • TMSTileFilter

  • DistanceToPointFilter

geodjango/urls.py設定

URLを設定します

(env) $ vi geodjango/urls.py
from django.contrib.gis import admin
from django.urls import include, path
from rest_framework.routers import DefaultRouter

from world.views import BorderViewSet, SchoolViewSet, FacilityViewSet, BusstopViewSet

router = DefaultRouter()
router.register('border', BorderViewSet)
router.register('school', SchoolViewSet)
router.register('facility', FacilityViewSet)
router.register('busstop', BusstopViewSet)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
]

router.registerにURLの接尾辞とViewを指定します。これをinclude(router.urls)で追加することで/api/配下のルーティングルールを登録します。

Browsable APIによる確認

Django REST frameworkのBrowsable APIを利用して、作成したREST APIの確認をします。

Webサーバを立ち上げて、http://localhost:8000/api/ にアクセスします。

(env) $ python manage.py runserver

行政区域データ

指定した点からの距離で絞り込むフィルタを指定

小学校区データ

指定した点からの距離で絞り込むフィルタを指定

公共施設データ

指定した点からの距離で絞り込むフィルタを指定

バス停留所データ

指定した点からの距離で絞り込むフィルタを指定

ページサイズとページ番号を指定

バウンダリを指定

データIDを指定

GeoJSON Serializer

GeoJSON Serializerを使ってLeafletでGeoJSONをマップに表示します。

編集対象ファイル

├── geodjango
│   └── urls.py              <-- マップページとREST APIのURL設定
└── world
    ├── static
    │   └── world
    │       ├── css
    │       │   └── app.css  <-- マップページのCSS
    │       └── js
    │           └── app.js   <-- マップページのJavaScript
    ├── templates
    │   └── world
    │       └── index.html   <-- マップページのテンプレートHTMLファイル
    └── views.py             <-- マップページとREST APIのビュー

Note staticとtemplatesは、フレームワークで決められた静的データとテンプレートの置き場所で(geodjango/settings.pyで変更可能)、アプリケーション名の下にそれぞれのファイルを配置します。

geodjango/urls.py設定

2つのURLを設定します。

  • ルート”/”でマップ表示するURL

  • REST APIでGeoJSONをgetするURL

URLを設定します

(env) $ vi geodjango/urls.py
from world.views import index, GeojsonAPIView
from django.views.generic.base import RedirectView

urlpatterns = [
    :
    path('', index, name='world_index'),
    path('world/geojson/', GeojsonAPIView.as_view(), name='geojson_view'),
]

static/world/css/app.css編集

マップページのCSSを記述します。

(env) $ vi static/world/css/app.css
html, body  {
    width: 100%;
    height: 100%;
    padding: 0px;
    margin: 0px;
}
#map {
    width: 100%;
    height: 100%;
}

static/world/js/app.js編集

マップページのJavaSvriptを記述します。

(env) $ vi static/world/js/app.js
// 地理院地図 標準地図
var std = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
    {id: 'stdmap', attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>国土地理院</a>"})
// 地理院地図 淡色地図
var pale = L.tileLayer('http://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
    {id: 'palemap', attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>国土地理院</a>"})
// OSM Japan
var osmjp = L.tileLayer('http://tile.openstreetmap.jp/{z}/{x}/{y}.png',
    { id: 'osmmapjp', attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' });
// OSM本家
var osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    { id: 'osmmap', attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' });

var baseMaps = {
    "地理院地図 標準地図" : std,
    "地理院地図 淡色地図" : pale,
    "OSM" : osm,
    "OSM japan" : osmjp
};

var map = L.map('map', {layers: [pale]});
map.setView([43.062083, 141.354389], 12);

// コントロールはオープンにする
L.control.layers(baseMaps, null, {collapsed:false}).addTo(map);

//スケールコントロールを追加(オプションはフィート単位を非表示)
L.control.scale({imperial: false}).addTo(map);

/* GeoJSONレイヤーを追加します */
$.getJSON("/world/geojson/", function(data) {
    L.geoJson(data).addTo(map);
});

templates/world/index.html編集

マップページのHTMLを記述します。

(env) $ templates/world/index.html
{% load staticfiles %}

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8" />
    <title>GeoDjango Hands-on</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <script
        src="https://code.jquery.com/jquery-3.3.1.min.js"
        integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
        crossorigin="anonymous"></script>

    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"
    integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ=="
    crossorigin=""/>

    <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"
    integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw=="
    crossorigin=""></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-tilelayer-geojson/1.0.4/TileLayer.GeoJSON.min.js"></script>

</head>
<body>
    <div id="map"></div>
    <link rel="stylesheet" href="{% static 'world/css/app.css' %}">
    <script type="text/javascript" src="{% static 'world/js/app.js' %}"></script>
</body>
</html>

world/views.py編集

2つのビューを作成します。

  • REST APIでGeoJSONを返すビュー

  • マップ表示するビュー

REST APIでGeoJSONを返すビューを作成。札幌市中央区のポリゴンを返します。

(env) $ vi world/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
import traceback
import json
from django.core.serializers import serialize

class GeojsonAPIView(APIView):
    def get(self, request, *args, **keywords):
        try:
            encjson  = serialize('geojson', Border.objects.filter(n03_004="中央区"),srid=4326, geometry_field='geom', fields=('n03_003','n03_004',) )
            result   = json.loads(encjson)
            response = Response(result, status=status.HTTP_200_OK)
        except Exception as e:
            traceback.print_exc()
            response = Response({}, status=status.HTTP_404_NOT_FOUND)
        except:
            response = Response({}, status=status.HTTP_404_NOT_FOUND)

        return response

マップ表示するビューを作成

(env) $ vi world/views.py
from django.shortcuts import render

def index(request):
    contexts = {}

    return render(request,'world/index.html',contexts)

ユーザ認証

Webサイトで公開した場合に誰でもアクセス可能な状態です。 サイト閲覧を権限を管理するためにアクセス制限機能をつけます。

編集対象ファイル

├── geodjango
│   ├── settings.py      <-- ログインURLを指定
│   └── urls.py          <-- URLを設定
├── templates
│   └── registration
│       └── login.html   <-- ユーザ向けログイン画面
└── world
    └── views.py         <-- ユーザ認証が必要な関数を指定

Note

  • ユーザ管理は、管理画面 http://127.0.0.1:8000/admin/ で行います。

  • ユーザ認証はセッションで行ってます。

  • デフォルトのセッション有効期間は2週間です

参考サイト

ユーザー認証の機能はDjangoの標準で用意されています。ユーザ向けに表示するHTML等は別途作成する必要があります。

accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']

テンプレートを読み込むディレクトリを追加

(env) $ vi geodjango/settings.py
TEMPLATES = [
    {
        'DIRS': [os.path.join(BASE_DIR, 'templates')], 
         :
  :

ログイン関連のURLを設定します

(env) $ vi geodjango/settings.py
LOGIN_URL='/accounts/login' <-- ログインURL
LOGIN_REDIRECT_URL='/'      <-- ログイン後トップページにリダイレクト
LOGOUT_REDIRECT_URL='/'     <-- ログアウト後トップページにリダイレクト

アカウントにURLを追加

(env) $ vi geodjango/urls.py
 urlpatterns = [
     :
    path('accounts/', include('django.contrib.auth.urls')),
     :

アクセス制限させたい関数に@login_requiredアノテーションをつけます

(env) $ vi world/views.py
from django.contrib.auth.decorators import login_required

@login_required   <-- アノテーションをつける
def index(request):
  :

ログイン画面のHTMLを作成します

(env) $ vi templates/registration/login.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <section class="common-form">
    {% if form.errors %}
        <p class="error-msg">Your username and password didn't match. Please try again.</p>
    {% endif %}

    {% if next %}
        {% if user.is_authenticated %}
        <p class="error-msg">Your account doesn't have access to this page. To proceed,
            please login with an account that has access.</p>
        {% else %}
            <p class="error-msg">Please login to see this page.</p>
        {% endif %}
    {% endif %}

    <form method="post" action="{% url 'login' %}">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="submit">Login</button>
        <input type="hidden" name="next" value="{{ next }}"/>
    </form>
    </section>
</body>
</html>

メインページで、http://127.0.0.1:8000/にアクセスした時に、ログインしてない時は、ログイン画面に遷移します。

Last updated