はじめに
この記事はMIERUNE Advent Calendar 2021 2日目の記事です。今回はGISに絡んだ記事を書いてみたいと思っていたので、GeoDjangoを触ってみよう!というテーマで執筆させていただきました。
GeoDjangoとは
Djangoが持つモジュールで、利用することでDjangoを通して地理空間データを扱うことができるようになります。Djangoそのものに含まれているモジュールです(ソースコード)。
Django公式ドキュメントのトップページでも紹介されており、そこでは以下のように記載されています。
GeoDjango は世界規模の地理情報 Web フレームワークを目指しています。 GeoDjango の目的は、地理情報システム (GIS) のWeb アプリケーションの開発をより簡単にし、空間データ (spatially enabled data) を活用することにあります。
GeoDjangoではGDAL APIをサポートしており、Djangoを介して一部のGDALの機能を利用することができます。
前提
この記事では、Django公式ドキュメントのGeoDjangoチュートリアルに則って、内容をかいつまんで進めてみます。
対象
- Djangoはある程度触ったことがある
- GeoDjangoははじめて
バージョン
python_version = "3.8"
django = "==3.2.6"
psycopg2-binary = "==2.9.1"
ソースコード/ 環境
以下に実行したソースコードを載せておきます。
https://github.com/selfsryo/GeoDjangoOfficialTutorial
GeoDjangoを扱うには当然Djangoのインストールと、地理空間用のDB(PostGIS等)が必要になります。今回はDockerでDjango + PostGISの環境を構築しました。GDALなどのGISに必要なライブラリもインストールしています。
以下のような構成になっています。
GeoDjangoOfficialTutorial
├── .gitignore
├── docker-compose.yml
├── geodjango
│ ├── .env.sample
│ ├── Dockerfile
│ ├── Pipfile
│ ├── entrypoint.sh
│ ├── geodjango
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── manage.py
│ └── world
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── data
│ │ ├── Readme.txt
│ │ ├── TM_WORLD_BORDERS-0.3.dbf
│ │ ├── TM_WORLD_BORDERS-0.3.prj
│ │ ├── TM_WORLD_BORDERS-0.3.shp
│ │ └── TM_WORLD_BORDERS-0.3.shx
│ ├── load.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── postgres
├── .env.db.sample
├── Dockerfile
└── sql
└── init.sql
やってみよう
準備
まずはgeodjango
というDjangoのプロジェクトを作成します。Dockerを使っている場合はコンテナの中に入るなりして実行します。
$ django-admin startproject geodjango
その後、world
というDjangoのアプリケーションを作成します。
$ cd geodjango
$ python3 manage.py startapp world
settings.py
のINSTALLED_APP
に'django.contrib.gis'
と、先ほど作成したworld
を追加します。なお、自分の場合はDockerを使っているので、いくつかの値を環境変数としてgeodjango/.env
から読み込むようにしています。DATABASESの設定値もDockerのPostGISに接続するようにしています。以上を踏まえ、settings.py
は以下のようになりました。
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# 修正
SECRET_KEY = os.environ.get("SECRET_KEY")
# 修正
DEBUG = int(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 追加
'django.contrib.gis',
'world',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'geodjango.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'geodjango.wsgi.application'
# 修正
DATABASES = {
'default': {
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.contrib.gis.db.backends.postgis'),
'NAME': os.environ.get('DATABASE_DB', 'postgres'),
'USER': os.environ.get('DATABASE_USER', 'postgres'),
'PASSWORD': os.environ.get('DATABASE_PASSWORD', 'password1234'),
'HOST': os.environ.get('DATABASE_HOST', 'postgres'),
'PORT': os.environ.get('DATABASE_PORT', '5432'),
}
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# 修正
LANGUAGE_CODE = 'ja'
# 修正
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
その後今回使用する国境データを用意します。データはこちら(クリックするとzipがダウンロードされます)から入手することができます。先ほど作成したアプリケーションのworld
の中にdata
というフォルダを作り、その中で落としたzipを解凍します。中にはSharpefile
が入っています(各ファイルの説明はこちら参照)。解凍後、zipは削除してしまいましょう。ファイルの階層は以下のようになります。
world
├── data
│ ├── Readme.txt
│ ├── TM_WORLD_BORDERS-0.3.dbf
│ ├── TM_WORLD_BORDERS-0.3.prj
│ ├── TM_WORLD_BORDERS-0.3.shp
│ └── TM_WORLD_BORDERS-0.3.shx
│
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
ogrinfo
ここで、ogrinfo
で国境データのShapefileの情報を覗いてみます。先ほど紹介したリポジトリからDockerを利用していると、コンテナの中でogrinfo
を用いることができます。
まずは以下を実行してメタデータを確認します。
$ ogrinfo world/data/TM_WORLD_BORDERS-0.3.shp
INFO: Open of `world/data/TM_WORLD_BORDERS-0.3.shp'
using driver `ESRI Shapefile' successful.
1: TM_WORLD_BORDERS-0.3 (Polygon)
TM_WORLD_BORDERS-0.3
というレイヤを持っていることがわかるので、詳細を出力してみます。レイヤ名を指定して-so
をつけて実行します。
$ ogrinfo -so world/data/TM_WORLD_BORDERS-0.3.shp TM_WORLD_BORDERS-0.3
INFO: Open of `world/data/TM_WORLD_BORDERS-0.3.shp'
using driver `ESRI Shapefile' successful.
Layer name: TM_WORLD_BORDERS-0.3
Metadata:
DBF_DATE_LAST_UPDATE=2008-07-30
Geometry: Polygon
Feature Count: 246
Extent: (-180.000000, -90.000000) - (180.000000, 83.623596)
Layer SRS WKT:
GEOGCRS["WGS 84",
DATUM["World Geodetic System 1984",
ELLIPSOID["WGS 84",6378137,298.257223563,
LENGTHUNIT["metre",1]]],
PRIMEM["Greenwich",0,
ANGLEUNIT["degree",0.0174532925199433]],
CS[ellipsoidal,2],
AXIS["latitude",north,
ORDER[1],
ANGLEUNIT["degree",0.0174532925199433]],
AXIS["longitude",east,
ORDER[2],
ANGLEUNIT["degree",0.0174532925199433]],
ID["EPSG",4326]]
Data axis to CRS axis mapping: 2,1
FIPS: String (2.0)
ISO2: String (2.0)
ISO3: String (3.0)
UN: Integer (3.0)
NAME: String (50.0)
AREA: Integer (7.0)
POP2005: Integer64 (10.0)
REGION: Integer (3.0)
SUBREGION: Integer (3.0)
LON: Real (8.3)
LAT: Real (7.3)
これで、レイヤ内の地物数(246)、空間参照系、および属性のタイプ等が確認できます。
モデルの定義
上の属性に合わせて、models.py
を以下のように定義します。modelsはdjango.db.models
ではなく、django.contrib.gis.db.models
からimportします。
from django.contrib.gis.db import models
class WorldBorder(models.Model):
# Regular Django fields corresponding to the attributes in the
# world borders shapefile.
name = models.CharField(max_length=50)
area = models.IntegerField()
pop2005 = models.IntegerField('Population 2005')
fips = models.CharField('FIPS Code', max_length=2, null=True)
iso2 = models.CharField('2 Digit ISO', max_length=2)
iso3 = models.CharField('3 Digit ISO', max_length=3)
un = models.IntegerField('United Nations Code')
region = models.IntegerField('Region Code')
subregion = models.IntegerField('Sub-Region Code')
lon = models.FloatField()
lat = models.FloatField()
# GeoDjango-specific: a geometry field (MultiPolygonField)
mpoly = models.MultiPolygonField()
# Returns the string representation of the model.
def __str__(self):
return self.name
mpoly(MultiPolygonField
)には何も指定していませんが、空間参照系はデフォルトでsrid=4326
となっています(参照)。他の空間参照系を指定したい場合は引数にsrid
で指定します。
models.pyを定義したら、以下のコマンドでマイグレーションを実行します。
$ python3 manage.py makemigrations
$ python3 manage.py migrate
インポート実行
先ほどダウンロードした国境データのShapefileをDBにインポートする方法はいくつかありますが、今回はGeoDjangoに備わっているLayerMapping
を利用してみます(こちら参照)。
後ほどスクリプトで実行しますが、いったんDjango Shellを用いつつ手順を追っていきます。
$ python3 manage.py shell
先ほどダウンロードした国境データのShapefileをPythonのPathオブジェクトにします。
>>> from pathlib import Path
>>> import world
>>> world_shp = Path(world.__file__).resolve().parent / 'data' / 'TM_WORLD_BORDERS-0.3.shp'
続いて、django.contrib.gis.gdal.DataSource
(こちら)を利用して、国境データのShapefileを開きます。
>>> from django.contrib.gis.gdal import DataSource
>>> ds = DataSource(world_shp)
>>> ds
<django.contrib.gis.gdal.datasource.DataSource object at 0x7f0a6df717c0>
>>> print(ds)
/usr/src/app/world/data/TM_WORLD_BORDERS-0.3.shp (ESRI Shapefile)
レイヤを開いてみます。Shapefile
は1つのレイヤしか持てませんが、確認してみると先ほどogrinfo
で確認できたTM_WORLD_BORDERS-0.3
が表示されます。以下のlyr
はLayer
オブジェクト(こちら)です。
>>> print(len(ds))
>>> lyr = ds[0]
>>> lyr
<django.contrib.gis.gdal.layer.Layer object at 0x7f0a6df71700>
>>> print(lyr)
TM_WORLD_BORDERS-0.3
レイヤからジオメトリタイプや地物数、属性フィールドも確認できます。
>>> print(lyr.geom_type)
Polygon
>>> len(lyr)
246
>>> print(lyr.fields)
['FIPS', 'ISO2', 'ISO3', 'UN', 'NAME', 'AREA', 'POP2005', 'REGION', 'SUBREGION', 'LON', 'LAT']
このLayerオブジェクトに空間参照系が関連づいていた場合、srs
という属性を持ちます。これはSpatialRegerence
というオブジェクト(こちら)として定義されています。またproj
属性を確認することで、測地系がWGS84
だと判別できます。
>>> srs = lyr.srs
>>> srs
<django.contrib.gis.gdal.srs.SpatialReference object at 0x7f9b6bd8c670>
>>> print(srs)
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.0174532925199433,
AUTHORITY["EPSG","9122"]],
AXIS["Latitude",NORTH],
AXIS["Longitude",EAST],
AUTHORITY["EPSG","4326"]]
>>> srs.proj
'+proj=longlat +datum=WGS84 +no_defs'
レイヤから任意の地物を取得してみます。これはFeature
オブジェクト(こちら)として取得することができます。
>>> feat = lyr[234]
>>> feat
<django.contrib.gis.gdal.feature.Feature object at 0x7f9b6bd14970>
>>> print(feat.get('NAME'))
San Marino
Feature
からはジオメトリを取得でき、WKTおよびGeoJSONとして出力することが可能です。
>>> geom = feat.geom
>>> print(geom.wkt)
POLYGON ((12.415798 43.957954,12.450554 43.979721,12.453888 43.981667,12.4625 43.984718,12.471666 43.986938,12.492777 43.989166,12.505554 43.988609,12.509998 43.986938,12.510277 43.982773,12.511665 43.943329,12.510555 43.939163,12.496387 43.923332,12.494999 43.914719,12.487778 43.90583,12.474443 43.897217,12.464722 43.895554,12.459166 43.896111,12.416388 43.904716,12.412222 43.906105,12.407822 43.913658,12.403889 43.926666,12.404999 43.948326,12.408888 43.954994,12.415798 43.957954))
>>> print(geom.json)
{ "type": "Polygon", "coordinates": [ [ [ 12.415798, 43.957954 ], [ 12.450554, 43.979721 ], [ 12.453888, 43.981667 ], [ 12.4625, 43.984718 ], [ 12.471666, 43.986938 ], [ 12.492777, 43.989166 ], [ 12.505554, 43.988609 ], [ 12.509998, 43.986938 ], [ 12.510277, 43.982773 ], [ 12.511665, 43.943329 ], [ 12.510555, 43.939163 ], [ 12.496387, 43.923332 ], [ 12.494999, 43.914719 ], [ 12.487778, 43.90583 ], [ 12.474443, 43.897217 ], [ 12.464722, 43.895554 ], [ 12.459166, 43.896111 ], [ 12.416388, 43.904716 ], [ 12.412222, 43.906105 ], [ 12.407822, 43.913658 ], [ 12.403889, 43.926666 ], [ 12.404999, 43.948326 ], [ 12.408888, 43.954994 ], [ 12.415798, 43.957954 ] ] ] }
少し長くなりましたが、上でやったことを踏まえ、国境データのShapefileをDBにインポートするようなスクリプトを書いてみます。world
以下にload.py
というモジュールを作成し、以下のようにします。
from pathlib import Path
from django.contrib.gis.utils import LayerMapping
from .models import WorldBorder
world_mapping = {
'fips' : 'FIPS',
'iso2' : 'ISO2',
'iso3' : 'ISO3',
'un' : 'UN',
'name' : 'NAME',
'area' : 'AREA',
'pop2005' : 'POP2005',
'region' : 'REGION',
'subregion' : 'SUBREGION',
'lon' : 'LON',
'lat' : 'LAT',
'mpoly' : 'MULTIPOLYGON',
}
world_shp = Path(__file__).resolve().parent / 'data' / 'TM_WORLD_BORDERS-0.3.shp'
def run(verbose=True):
lm = LayerMapping(WorldBorder, world_shp, world_mapping, transform=False)
lm.save(strict=True, verbose=verbose)
world_mapping
という辞書では、キーにWorldBorder
モデルのフィールド、値に国境データのShapefileの持つ属性がマップされています。また今回は座標系を移動させる必要がないため、transform=False
とします。
このスクリプトを実行してみます。先ほどと同じくDjango Shellを実行します。
$ python3 manage.py shell
続いて、以下のコマンドを叩きます。
>>> from world import load
>>> load.run()
実行するとSaved:国の名前
と出力されるはずです。WorldBorder
モデルの中で__str__()
を定義しているため、name属性が表示されるようになっています。(__str__()
をコメントアウトするとWorldBorder object (〇〇)
と出力されます)
Saved: Antigua and Barbuda
Saved: Algeria
Saved: Azerbaijan
Saved: Albania
Saved: Armenia
Saved: Angola
Saved: American Samoa
Saved: Argentina
Saved: Australia
・・・・
ogrinspect
Djangoには、ispectdb
という便利なコマンドがあり、これを実行するとDBのmodesl.pyを自動で出力してくれます。GeoDjangoではベクターデータに対応したコマンドとして、ogrinspect
というコマンドが用意されています(こちら)。
今回の場合だと、以下のようなコマンドを実行します。
$ python3 manage.py ogrinspect world/data/TM_WORLD_BORDERS-0.3.shp WorldBorder --srid=4326 --mapping --multi
結果、国境データのShapefileからそれに合ったmodels.pyの結果を出力してくれます。
from django.contrib.gis.db import models
class WorldBorder(models.Model):
fips = models.CharField(max_length=2)
iso2 = models.CharField(max_length=2)
iso3 = models.CharField(max_length=3)
un = models.IntegerField()
name = models.CharField(max_length=50)
area = models.IntegerField()
pop2005 = models.BigIntegerField()
region = models.IntegerField()
subregion = models.IntegerField()
lon = models.FloatField()
lat = models.FloatField()
geom = models.MultiPolygonField(srid=4326)
# Auto-generated `LayerMapping` dictionary for WorldBorder model
worldborder_mapping = {
'fips': 'FIPS',
'iso2': 'ISO2',
'iso3': 'ISO3',
'un': 'UN',
'name': 'NAME',
'area': 'AREA',
'pop2005': 'POP2005',
'region': 'REGION',
'subregion': 'SUBREGION',
'lon': 'LON',
'lat': 'LAT',
'geom': 'MULTIPOLYGON',
}
なお、このコマンドはsettings.py
のINSTALLED_APPS
に'django.contrib.gis'
を含めておかないと実行できないので注意が必要です。
クエリセットAPI
DjangoのORMには多彩なコマンド(クエリセットAPIと呼ばれています。こちら参照)が用意されています。またこの中で、SQLのWHERE句に相当する機能としてlookupsと呼ばれる構文を指定することができ、これらを利用することでSQLを叩かずとも欲しいクエリセットを取得することができます。
そしてGeoDjangoでは、通常とは別に、GISとしての利用に特化した多彩なlookupsが用意されています(こちら参照)。試しに実行してみます。再度Django Shellを起動します。
$ python3 manage.py shell
起動したら、WKT形式(経度 緯度)で富士山のPOINT
を定義します。
>>> pnt_wkt = 'POINT(138.729952 35.359455)'
このPOINT
を含んでいるインスタンスのクエリセットを取得してみます。contains
というlookups(こちら)を指定します。通常のDjangoでもcontainsというlookupsは存在しますが、GeoDjangoではその地点を含むクエリセットを返してくれます。
>>> from world.models import WorldBorder
>>> WorldBorder.objects.filter(mpoly__contains=pnt_wkt)
<QuerySet [<WorldBorder: Japan>]>
WKT形式ではなく、GEOS
オブジェクト(こちら)としてもクエリセットを取得できます。同じく富士山のPointを定義します。
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(138.729952,35.359455)
次はintersects
というlookups(こちら)を利用してみます。getと組み合わせて、空間的に交差するインスタンスを取得することができます。
>>> WorldBorder.objects.get(mpoly__intersects=pnt)
<WorldBorder: Japan>
最後に富士山から1,000km以内に存在するインスタンスのクエリセットを取得してみます。distance_lt
(こちら)というlookupsを用いて、距離を基準にフィルタすることができます。
>>> from django.contrib.gis.measure import D
>>> WorldBorder.objects.filter(mpoly__distance_lt=(pnt_wkt, D(km=1000)))
<QuerySet [<WorldBorder: Korea, Republic of>, <WorldBorder: Japan>, <WorldBorder: Korea, Democratic People's Republic of>, <WorldBorder: Russia>]>
このように、GeoDjangoにはとても便利なクエリセットAPIがたくさん用意されています。
座標参照系の自動変換
GeoDjangoでクエリを実行する際、座標参照系の変換も自動的に行ってくれます。srid=3857
に指定して、先ほどと同じ富士山の座標を定義します。
>>> from world.models import WorldBorder
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(15443347.614415286, 4212837.5728517035, srid=3857)
この座標を利用して、srid=4326
で登録されている今のモデルに対してクエリを叩きます。先ほどと同じ結果が返ってきます。
>>> from django.contrib.gis.measure import D
>>> WorldBorder.objects.filter(mpoly__distance_lt=(pnt, D(km=1000)))
<QuerySet [<WorldBorder: Korea, Republic of>, <WorldBorder: Japan>, <WorldBorder: Korea, Democratic People's Republic of>, <WorldBorder: Russia>]>
内部的には以下のようなSQLが実行されています。DjangoのORMが裏側で変換をしてくれているのがわかります。
>>> qs = WorldBorder.objects.filter(mpoly__distance_lt=(pnt, D(km=1000)))
>>> print(qs.query)
SELECT "world_worldborder"."id", "world_worldborder"."name", "world_worldborder"."area", "world_worldborder"."pop2005", "world_worldborder"."fips", "world_worldborder"."iso2", "world_worldborder"."iso3", "world_worldborder"."un", "world_worldborder"."region", "world_worldborder"."subregion", "world_worldborder"."lon", "world_worldborder"."lat", "world_worldborder"."mpoly"::bytea FROM "world_worldborder" WHERE ST_DistanceSphere("world_worldborder"."mpoly", ST_Transform(ST_GeomFromEWKB('\001\001\000\000 \021\017\000\000?J\251s\262tmA1\232\251d\031\022PA'::bytea), 4326)) < 1000000.0
GeoDjangoにおける遅延評価
Djangoではクエリの実行において、実際に値が必要になるまでクエリを叩かない、という最適化手法を取っています。これを遅延評価といいます(参考)。それとは若干異なるのですが、GeoDjangoでは実際にインスタンスのジオメトリフィールドにアクセスした際、初めてオブジェクトが作成されるような仕様になっています。
以下ではjp
というインスタンスを取得しています。この際、GeoDjangoはGEOSGeometry
オブジェクト(こちら)を作成します。
>>> jp = WorldBorder.objects.get(name='Japan')
>>> jp.mpoly
<MultiPolygon object at 0x7f23279cdd90>
このジオメトリオブジェクトは、様々な地理空間的属性およびメソッドを兼ね備えています。
>>> jp.mpoly.wkt
'MULTIPOLYGON (((153.958588 24.294998, 153.953308 24.292774, 153.946625 24.293331, 153.942749 24.296944, 153.939697 24.300831,
・・・・・
>>> jp.mpoly.geojson
'{ "type": "MultiPolygon", "coordinates": [ [ [ [ 153.958588, 24.294998 ], [ 153.953308, 24.292774 ], [ 153.946625, 24.293331 ], [ 153.942749, 24.296944 ], [ 153.939697, 24.300831 ],
・・・・・
>>> from django.contrib.gis.geos import Point
>>> pnt = Point(138.729952,35.359455)
>>> jp.mpoly.contains(pnt)
True
アドミンページ
Djangoの非常に強力な機能の一つにアドミンページがあります。GeoDjangoでは、このアドミンページに関しても、地理空間的に特化した機能が搭載されています。
さっそく試してみましょう。world
の中のadmin.py
を以下のようにします。django.contrib.admin
ではなく、django.contrib.gis.admin
(こちら)をインポートするのがポイントです。
from django.contrib.gis import admin
from .models import WorldBorder
admin.site.register(WorldBorder, admin.GeoModelAdmin)
あとは以下のコマンドでアドミンユーザーを作成します。
$ python3 manage.py createsuperuser
実行が終わったらhttp://localhost:8000/admin/
にアクセスします。ユーザー情報を入力してログインし、WorldBorder
の任意のオブジェクト編集画面に遷移します。以下のように、Djangoのアドミンページから国境を編集することができます。
その他ライブラリを導入することで背景マップを変更ができたりします。アドミンページにも様々な機能が実装されています。
おわりに
改めてGeoDjangoを触ってみることで、まだまだたくさんの強力な機能があることに気付けました。今後もGeoDjangoの素晴らしさを紹介していくことができたらと思います。