PostGISに入力したデータを使った処理を作成して行きたいと思います。
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.2djangorestframework-gis==0.13django-filter==1.1.0Markdown==2.6.11
Note
leaflet.js - https://leafletjs.com/
Django REST framework(DRF) - http://www.django-rest-framework.org/
django-rest-framework-gis - https://github.com/djangonauts/django-rest-framework-gis
django-filter - https://django-filter.readthedocs.io/en/latest/index.html
markdown - https://python-markdown.github.io/
編集対象ファイル
├── geodjango│ ├── settings.py <-- 設定│ └── urls.py <-- REST APIのURL設定└── world├── serializers.py <-- REST APIで使うシリアライザー└── views.py <-- REST APIのビュー
インストールしたアプリケーションを設定ファイルのsettings.pyに追加します
(env) $ vi geodjango/settings.pyINSTALLED_APPS = [:'django_filters','rest_framework','rest_framework_gis','markdown',]
シリアライザはデータベースとAPIのとの間でデータフォーマットの変換をします。 worldアプリにserializers.pyファイルを作成します。
(env) $ vi world/serializers.pyfrom rest_framework import serializersfrom .models import Border, School, Facility, Busstopclass BorderSerializer(serializers.ModelSerializer):class Meta:model = Borderfields = ('__all__')class SchoolSerializer(serializers.ModelSerializer):class Meta:model = Schoolfields = ('__all__')class FacilitySerializer(serializers.ModelSerializer):class Meta:model = Facilityfields = ('__all__')class BusstopSerializer(serializers.ModelSerializer):class Meta:model = Busstopexclude = ("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")
ビューでリクエストに対するレスポンスの設定をします。
(env) $ vi world/views.pyfrom rest_framework import viewsetsfrom rest_framework_gis.filters import DistanceToPointFilter, InBBoxFilterfrom rest_framework.pagination import PageNumberPaginationfrom .serializers import BorderSerializer, SchoolSerializer, FacilitySerializer, BusstopSerializerfrom .models import Border, School, Facility, Busstopclass MyPagination(PageNumberPagination):page_size_query_param = 'page_size'class BorderViewSet(viewsets.ModelViewSet):queryset = Border.objects.all()serializer_class = BorderSerializerpagination_class = MyPaginationfilter_backends = (DistanceToPointFilter,)distance_filter_field = 'geom'distance_filter_convert_meters = Trueclass SchoolViewSet(viewsets.ModelViewSet):queryset = School.objects.all()serializer_class = SchoolSerializerpagination_class = MyPaginationfilter_backends = (DistanceToPointFilter,)distance_filter_field = 'geom'distance_filter_convert_meters = Trueclass FacilityViewSet(viewsets.ModelViewSet):queryset = Facility.objects.all()serializer_class = FacilitySerializerpagination_class = MyPaginationfilter_backends = (DistanceToPointFilter,)distance_filter_field = 'geom'distance_filter_convert_meters = Falseclass BusstopViewSet(viewsets.ModelViewSet):queryset = Busstop.objects.all()serializer_class = BusstopSerializerpagination_class = MyPaginationfilter_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
URLを設定します
(env) $ vi geodjango/urls.pyfrom django.contrib.gis import adminfrom django.urls import include, pathfrom rest_framework.routers import DefaultRouterfrom world.views import BorderViewSet, SchoolViewSet, FacilityViewSet, BusstopViewSetrouter = 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/配下のルーティングルールを登録します。
Django REST frameworkのBrowsable APIを利用して、作成したREST APIの確認をします。
Webサーバを立ち上げて、http://localhost:8000/api/ にアクセスします。
(env) $ python manage.py runserver
指定した点からの距離で絞り込むフィルタを指定
指定した点からの距離で絞り込むフィルタを指定
指定した点からの距離で絞り込むフィルタを指定
指定した点からの距離で絞り込むフィルタを指定
ページサイズとページ番号を指定
バウンダリを指定
データIDを指定
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で変更可能)、アプリケーション名の下にそれぞれのファイルを配置します。
2つのURLを設定します。
ルート”/”でマップ表示するURL
REST APIでGeoJSONをgetするURL
URLを設定します
(env) $ vi geodjango/urls.pyfrom world.views import index, GeojsonAPIViewfrom django.views.generic.base import RedirectViewurlpatterns = [:path('', index, name='world_index'),path('world/geojson/', GeojsonAPIView.as_view(), name='geojson_view'),]
マップページのCSSを記述します。
(env) $ vi static/world/css/app.csshtml, body {width: 100%;height: 100%;padding: 0px;margin: 0px;}#map {width: 100%;height: 100%;}
マップページの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 Japanvar osmjp = L.tileLayer('http://tile.openstreetmap.jp/{z}/{x}/{y}.png',{ id: 'osmmapjp', attribution: '© <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: '© <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);});
マップページの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"><scriptsrc="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/[email protected]/dist/leaflet.css"integrity="sha512-Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJAAQ=="crossorigin=""/><script src="https://unpkg.com/[email protected]/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>
2つのビューを作成します。
REST APIでGeoJSONを返すビュー
マップ表示するビュー
REST APIでGeoJSONを返すビューを作成。札幌市中央区のポリゴンを返します。
(env) $ vi world/views.pyfrom rest_framework.views import APIViewfrom rest_framework.response import Responsefrom rest_framework import statusimport tracebackimport jsonfrom django.core.serializers import serializeclass 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.pyfrom django.shortcuts import renderdef 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週間です
参考サイト
Django2 でユーザー認証(ログイン認証)を実装するチュートリアル -2- サインアップとログイン・ログアウト - https://it-engineer-lab.com/archives/544
ユーザー認証の機能は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.pyTEMPLATES = [{'DIRS': [os.path.join(BASE_DIR, 'templates')],::
ログイン関連のURLを設定します
(env) $ vi geodjango/settings.pyLOGIN_URL='/accounts/login' <-- ログインURLLOGIN_REDIRECT_URL='/' <-- ログイン後トップページにリダイレクトLOGOUT_REDIRECT_URL='/' <-- ログアウト後トップページにリダイレクト
アカウントにURLを追加
(env) $ vi geodjango/urls.pyurlpatterns = [:path('accounts/', include('django.contrib.auth.urls')),:
アクセス制限させたい関数に@login_requiredアノテーションをつけます
(env) $ vi world/views.pyfrom 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/にアクセスした時に、ログインしてない時は、ログイン画面に遷移します。