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変換ライブラリ
1
(env) $ pip install djangorestframework-gis
2
(env) $ pip install django-filter # Filtering support
3
(env) $ pip install markdown # Markdown support for the browsable API.
4
(env) $ pip freeze
5
:
6
djangorestframework==3.8.2
7
djangorestframework-gis==0.13
8
django-filter==1.1.0
9
Markdown==2.6.11
Copied!
Note
編集対象ファイル
1
├── geodjango
2
│ ├── settings.py <-- 設定
3
│ └── urls.py <-- REST APIのURL設定
4
└── world
5
├── serializers.py   <-- REST APIで使うシリアライザー
6
└── views.py <-- REST APIのビュー
Copied!

geodjango/settings.py設定

インストールしたアプリケーションを設定ファイルのsettings.pyに追加します
1
(env) $ vi geodjango/settings.py
2
INSTALLED_APPS = [
3
:
4
'django_filters',
5
'rest_framework',
6
'rest_framework_gis',
7
'markdown',
8
]
Copied!

world/serializers.py作成

シリアライザはデータベースとAPIのとの間でデータフォーマットの変換をします。 worldアプリにserializers.pyファイルを作成します。
1
(env) $ vi world/serializers.py
2
from rest_framework import serializers
3
from .models import Border, School, Facility, Busstop
4
5
class BorderSerializer(serializers.ModelSerializer):
6
class Meta:
7
model = Border
8
fields = ('__all__')
9
10
class SchoolSerializer(serializers.ModelSerializer):
11
class Meta:
12
model = School
13
fields = ('__all__')
14
15
class FacilitySerializer(serializers.ModelSerializer):
16
class Meta:
17
model = Facility
18
fields = ('__all__')
19
20
class BusstopSerializer(serializers.ModelSerializer):
21
class Meta:
22
model = Busstop
23
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",
24
"p11_003_10", "p11_003_11", "p11_003_12", "p11_003_13", "p11_003_14", "p11_003_15",
25
"p11_003_16", "p11_003_17", "p11_003_18", "p11_003_19",
26
"p11_004_2", "p11_004_3", "p11_004_4", "p11_004_5", "p11_004_6", "p11_004_7", "p11_004_8", "p11_004_9",
27
"p11_004_10", "p11_004_11", "p11_004_12", "p11_004_13", "p11_004_14", "p11_004_15", "p11_004_16",
28
"p11_004_17", "p11_004_18", "p11_004_19")
Copied!

world/views.py作成

ビューでリクエストに対するレスポンスの設定をします。
1
(env) $ vi world/views.py
2
from rest_framework import viewsets
3
from rest_framework_gis.filters import DistanceToPointFilter, InBBoxFilter
4
from rest_framework.pagination import PageNumberPagination
5
6
from .serializers import BorderSerializer, SchoolSerializer, FacilitySerializer, BusstopSerializer
7
from .models import Border, School, Facility, Busstop
8
9
class MyPagination(PageNumberPagination):
10
page_size_query_param = 'page_size'
11
12
class BorderViewSet(viewsets.ModelViewSet):
13
queryset = Border.objects.all()
14
serializer_class = BorderSerializer
15
pagination_class = MyPagination
16
filter_backends = (DistanceToPointFilter,)
17
distance_filter_field = 'geom'
18
distance_filter_convert_meters = True
19
20
class SchoolViewSet(viewsets.ModelViewSet):
21
queryset = School.objects.all()
22
serializer_class = SchoolSerializer
23
pagination_class = MyPagination
24
filter_backends = (DistanceToPointFilter,)
25
distance_filter_field = 'geom'
26
distance_filter_convert_meters = True
27
28
class FacilityViewSet(viewsets.ModelViewSet):
29
queryset = Facility.objects.all()
30
serializer_class = FacilitySerializer
31
pagination_class = MyPagination
32
filter_backends = (DistanceToPointFilter,)
33
distance_filter_field = 'geom'
34
distance_filter_convert_meters = False
35
36
class BusstopViewSet(viewsets.ModelViewSet):
37
queryset = Busstop.objects.all()
38
serializer_class = BusstopSerializer
39
pagination_class = MyPagination
40
filter_backends = (DistanceToPointFilter, InBBoxFilter)
41
distance_filter_field = bbox_filter_field = 'geom'
42
distance_filter_convert_meters = True
Copied!
設定項目
  • 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を設定します
1
(env) $ vi geodjango/urls.py
2
from django.contrib.gis import admin
3
from django.urls import include, path
4
from rest_framework.routers import DefaultRouter
5
6
from world.views import BorderViewSet, SchoolViewSet, FacilityViewSet, BusstopViewSet
7
8
router = DefaultRouter()
9
router.register('border', BorderViewSet)
10
router.register('school', SchoolViewSet)
11
router.register('facility', FacilityViewSet)
12
router.register('busstop', BusstopViewSet)
13
14
urlpatterns = [
15
path('admin/', admin.site.urls),
16
path('api/', include(router.urls)),
17
]
Copied!
router.registerにURLの接尾辞とViewを指定します。これをinclude(router.urls)で追加することで/api/配下のルーティングルールを登録します。

Browsable APIによる確認

Django REST frameworkのBrowsable APIを利用して、作成したREST APIの確認をします。
Webサーバを立ち上げて、http://localhost:8000/api/ にアクセスします。
1
(env) $ python manage.py runserver
Copied!

行政区域データ

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

小学校区データ

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

公共施設データ

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

バス停留所データ

指定した点からの距離で絞り込むフィルタを指定
ページサイズとページ番号を指定
バウンダリを指定
データIDを指定

GeoJSON Serializer

GeoJSON Serializerを使ってLeafletでGeoJSONをマップに表示します。
編集対象ファイル
1
├── geodjango
2
│ └── urls.py <-- マップページとREST APIのURL設定
3
└── world
4
├── static
5
│ └── world
6
│ ├── css
7
│ │ └── app.css <-- マップページのCSS
8
│ └── js
9
│ └── app.js <-- マップページのJavaScript
10
├── templates
11
│ └── world
12
│ └── index.html <-- マップページのテンプレートHTMLファイル
13
└── views.py <-- マップページとREST APIのビュー
Copied!
Note staticとtemplatesは、フレームワークで決められた静的データとテンプレートの置き場所で(geodjango/settings.pyで変更可能)、アプリケーション名の下にそれぞれのファイルを配置します。

geodjango/urls.py設定

2つのURLを設定します。
  • ルート”/”でマップ表示するURL
  • REST APIでGeoJSONをgetするURL
URLを設定します
1
(env) $ vi geodjango/urls.py
2
from world.views import index, GeojsonAPIView
3
from django.views.generic.base import RedirectView
4
5
urlpatterns = [
6
:
7
path('', index, name='world_index'),
8
path('world/geojson/', GeojsonAPIView.as_view(), name='geojson_view'),
9
]
Copied!

static/world/css/app.css編集

マップページのCSSを記述します。
1
(env) $ vi static/world/css/app.css
2
html, body {
3
width: 100%;
4
height: 100%;
5
padding: 0px;
6
margin: 0px;
7
}
8
#map {
9
width: 100%;
10
height: 100%;
11
}
Copied!

static/world/js/app.js編集

マップページのJavaSvriptを記述します。
1
(env) $ vi static/world/js/app.js
2
// 地理院地図 標準地図
3
var std = L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png',
4
{id: 'stdmap', attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>国土地理院</a>"})
5
// 地理院地図 淡色地図
6
var pale = L.tileLayer('http://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
7
{id: 'palemap', attribution: "<a href='http://portal.cyberjapan.jp/help/termsofuse.html' target='_blank'>国土地理院</a>"})
8
// OSM Japan
9
var osmjp = L.tileLayer('http://tile.openstreetmap.jp/{z}/{x}/{y}.png',
10
{ id: 'osmmapjp', attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' });
11
// OSM本家
12
var osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
13
{ id: 'osmmap', attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors' });
14
15
var baseMaps = {
16
"地理院地図 標準地図" : std,
17
"地理院地図 淡色地図" : pale,
18
"OSM" : osm,
19
"OSM japan" : osmjp
20
};
21
22
var map = L.map('map', {layers: [pale]});
23
map.setView([43.062083, 141.354389], 12);
24
25
// コントロールはオープンにする
26
L.control.layers(baseMaps, null, {collapsed:false}).addTo(map);
27
28
//スケールコントロールを追加(オプションはフィート単位を非表示)
29
L.control.scale({imperial: false}).addTo(map);
30
31
/* GeoJSONレイヤーを追加します */
32
$.getJSON("/world/geojson/", function(data) {
33
L.geoJson(data).addTo(map);
34
});
Copied!

templates/world/index.html編集

マップページのHTMLを記述します。
1
(env) $ templates/world/index.html
2
{% load staticfiles %}
3
4
<!DOCTYPE html>
5
<html lang="ja">
6
<head>
7
<meta charset="utf-8" />
8
<title>GeoDjango Hands-on</title>
9
<meta name="viewport" content="width=device-width, initial-scale=1.0">
10
11
<script
12
src="https://code.jquery.com/jquery-3.3.1.min.js"
13
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
14
crossorigin="anonymous"></script>
15
16
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
17
integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ=="
18
crossorigin=""/>
19
20
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"
21
integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw=="
22
crossorigin=""></script>
23
24
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-tilelayer-geojson/1.0.4/TileLayer.GeoJSON.min.js"></script>
25
26
</head>
27
<body>
28
<div id="map"></div>
29
<link rel="stylesheet" href="{% static 'world/css/app.css' %}">
30
<script type="text/javascript" src="{% static 'world/js/app.js' %}"></script>
31
</body>
32
</html>
Copied!

world/views.py編集

2つのビューを作成します。
  • REST APIでGeoJSONを返すビュー
  • マップ表示するビュー
REST APIでGeoJSONを返すビューを作成。札幌市中央区のポリゴンを返します。
1
(env) $ vi world/views.py
2
from rest_framework.views import APIView
3
from rest_framework.response import Response
4
from rest_framework import status
5
import traceback
6
import json
7
from django.core.serializers import serialize
8
9
class GeojsonAPIView(APIView):
10
def get(self, request, *args, **keywords):
11
try:
12
encjson = serialize('geojson', Border.objects.filter(n03_004="中央区"),srid=4326, geometry_field='geom', fields=('n03_003','n03_004',) )
13
result = json.loads(encjson)
14
response = Response(result, status=status.HTTP_200_OK)
15
except Exception as e:
16
traceback.print_exc()
17
response = Response({}, status=status.HTTP_404_NOT_FOUND)
18
except:
19
response = Response({}, status=status.HTTP_404_NOT_FOUND)
20
21
return response
Copied!
マップ表示するビューを作成
1
(env) $ vi world/views.py
2
from django.shortcuts import render
3
4
def index(request):
5
contexts = {}
6
7
return render(request,'world/index.html',contexts)
Copied!

ユーザ認証

Webサイトで公開した場合に誰でもアクセス可能な状態です。 サイト閲覧を権限を管理するためにアクセス制限機能をつけます。
編集対象ファイル
1
├── geodjango
2
│ ├── settings.py <-- ログインURLを指定
3
│ └── urls.py <-- URLを設定
4
├── templates
5
│ └── registration
6
│ └── login.html <-- ユーザ向けログイン画面
7
└── world
8
└── views.py <-- ユーザ認証が必要な関数を指定
Copied!
Note
  • ユーザ管理は、管理画面 http://127.0.0.1:8000/admin/ で行います。
  • ユーザ認証はセッションで行ってます。
  • デフォルトのセッション有効期間は2週間です
参考サイト
ユーザー認証の機能はDjangoの標準で用意されています。ユーザ向けに表示するHTML等は別途作成する必要があります。
1
accounts/login/ [name='login']
2
accounts/logout/ [name='logout']
3
accounts/password_change/ [name='password_change']
4
accounts/password_change/done/ [name='password_change_done']
5
accounts/password_reset/ [name='password_reset']
6
accounts/password_reset/done/ [name='password_reset_done']
7
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
8
accounts/reset/done/ [name='password_reset_complete']
Copied!
テンプレートを読み込むディレクトリを追加
1
(env) $ vi geodjango/settings.py
2
TEMPLATES = [
3
{
4
'DIRS': [os.path.join(BASE_DIR, 'templates')],
5
:
6
  :
Copied!
ログイン関連のURLを設定します
1
(env) $ vi geodjango/settings.py
2
LOGIN_URL='/accounts/login' <-- ログインURL
3
LOGIN_REDIRECT_URL='/' <-- ログイン後トップページにリダイレクト
4
LOGOUT_REDIRECT_URL='/' <-- ログアウト後トップページにリダイレクト
Copied!
アカウントにURLを追加
1
(env) $ vi geodjango/urls.py
2
urlpatterns = [
3
:
4
path('accounts/', include('django.contrib.auth.urls')),
5
:
Copied!
アクセス制限させたい関数に@login_requiredアノテーションをつけます
1
(env) $ vi world/views.py
2
from django.contrib.auth.decorators import login_required
3
4
@login_required <-- アノテーションをつける
5
def index(request):
6
:
Copied!
ログイン画面のHTMLを作成します
1
(env) $ vi templates/registration/login.html
2
<!DOCTYPE html>
3
<html>
4
<head>
5
<meta charset="utf-8">
6
<meta name="viewport" content="width=device-width, initial-scale=1">
7
<title>Login</title>
8
</head>
9
<body>
10
<h1>Login</h1>
11
<section class="common-form">
12
{% if form.errors %}
13
<p class="error-msg">Your username and password didn't match. Please try again.</p>
14
{% endif %}
15
16
{% if next %}
17
{% if user.is_authenticated %}
18
<p class="error-msg">Your account doesn't have access to this page. To proceed,
19
please login with an account that has access.</p>
20
{% else %}
21
<p class="error-msg">Please login to see this page.</p>
22
{% endif %}
23
{% endif %}
24
25
<form method="post" action="{% url 'login' %}">
26
{% csrf_token %}
27
{{ form.as_p }}
28
<button type="submit" class="submit">Login</button>
29
<input type="hidden" name="next" value="{{ next }}"/>
30
</form>
31
</section>
32
</body>
33
</html>
Copied!