Django, LeafletJS ve Postgres ile Web Haritası Yapımı — 1. Kısım

Volkan Dağdelen
10 min readMar 18, 2021

Bu yazımızda IBB Açık Veri Portalı’nın sağladığı İstanbul Sağlık Kurum ve Kuruluşlarına İlişkin Bilgiler isimli veri setini kullanarak basit bir web haritası üzerinde görselleştirmenin nasıl yapılacağını konuşacağız. Kullandığımız veriler IBB Açık veri lisansı altındadır ve 4.0 Uluslararası (CC BY 4.0) kapsamında lisanslanan kamu sektörü bilgilerini içerir, detaylı bilgi için tıklayınız.

Verilere JavaScript üzerinden erişilebilmek için gerekli olan backend yapısını Django aracılığı ile kodlayacağız ve böylece postgres ile oluşturulacak veri tabanına erişimi sağlamak için REST API yapısını oluşturacağız. Postgres’in seçilmesinin sebebi ise PostGIS extension’ı ile mekansal verilerin rahat bir şekilde tutulabilmesi ve mekansal operasyonlar için sağladığı metodların bulunmasıdır. Ayrıca REST API ile geoJSON döndürmeyi planlıyoruz bu sebepten dolayı postgres ve Django’nun geodjango yapısı büyük ölçüde işlerimizi kolaylaştıracaktır.

Projede kullanılacak teknolojilere ait linklere aşağıdan ulaşabilirsiniz.

Bu yazıdaki amaç kullanılan teknolojileri sıfırdan öğretmek değil, bu teknolojiler ile geliştirilebilecek uygulamalara örnek vermek ve ilham olabilmektir. Bu yüzden belirli bir seviyede bu teknolojilere, Python, JavaScript dillerine ve basit bir düzeyde veritabanı bilgisine, hakim olmanız önerilmektedir.

NOT: Kurulumlarda ve geliştirme sırasında eğer daha önceden gelen bir tecrübeniz bulunmuyor ise hatalar alma ihtimaliniz yüksektir. Lütfen aldığınız hataları google’dan aramayı deneyin. Eğer sorunu hala çözemiyorsanız veya yazıda bir hata farkederseniz lütfen iletişim kanalları aracılığı ile benimle iletişime geçmekten çekinmeyin.

🧰 Projeye ait github reposu

Backend Kısmı için Python Ortam Hazırlıkları

Photo by Fotis Fotopoulos on Unsplash

Python ile geliştirilen projelerde environment kullanılması büyük önem taşımaktadır. Python ile belirli bir geliştirme tecrübeniz var ise projenizde kullanacağınız kütüphanelerin kurulumu veya kütüphanelerin birbirini bozması konusunda sorunlar yaşamış olma ihtimaliniz yüksektir. Eğer environment kullanmazsanız bazı python kütüphaneleri arasındaki çakışmalardan dolayı bir çok kütüphanelerin bozulmasına sebep olabilirsiniz ve daha da ötesi bu bozulmaları düzeltmek oldukça zahmetli olabilir. Diğer bir sebep ise uygulamanızı deploy edecekseniz server tarafında uygulamanızın hangi kütüphanelere ihtiyaç olduğunun bilinmesi ve bu bağımlılıkların oluşturulması gerekir bunu da requirements.txt isimli şirin bir dosya ile hallediyoruz. Eğer environment kullanmıyorsanız pip freeze komutu ile oluşturacağınız requirements.txt dosyası uygulamanız için gerekli olmayan kütüphaneleri de içerecektir ve ayıklamaya uğraşmak oldukça zahmetli olacaktır.

Python ile environment oluşturmak için terminalinizi açarak

NOT: Linux veya macOS kullanıcısı iseniz python yerine python3 yazmanız gerekebilir

$ python -m venv env

yazmanız gerekmektedir. Bu komut ile proje klasörümüzde env isimli bir klasör oluşacaktır.

Oluşturduğumuz env isimli environment’ı aktif etmek için

// Ubuntu veya MacOS için
$ source env\Scripts\activate

Eğer windows kullanıyorsanız cmd üzerinden

// Windows için
> env\Scripts\activate

yazmanız gerekmektedir. Ardından gerekli kütüphaneleri kurmak için

NOT: Linux veya macOS kullanıcısı iseniz pip yerine pip3 yazmanız gerekebilir.

(env) $ pip install Django 
(env) $ pip install djangorestframework
(env) $ pip install psycopg2
(env) $ pip install django-cors-headers
(env) $ pip install djangorestframework-gis

Ayrıca GDAL ve geos’un da yüklenmesi geoDjango yapısının kullanılabilmesi için gereklilik taşımaktadır. Lütfen yükleme adımları için resmi dökümantasyona göz atın. Eğer windows kullanıyorsanız GDAL’ın yüklenmesi sizin için sıkıntı çıkarıyor olabilir. Lütfen google’dan araştırarak bu sorunu nasıl aşacağınızı araştırınız. Bu sorunun çözümü için bir çok yazı halihazırda mevcut.

Django Projesinin Başlatılması

Django projesi oluşturmak için terminal üzerinden

(env) $ django-admin startproject geoProject
(env) $ mkdir src

Django projesine uygulama eklemek için

(env) $ cd geoProject
(env) $ django-admin startapp geoApp

Bu kısma kadar sorunsuz geldiyseniz proje dosyalarınız şöyle görünmelidir.

|- env
|
|- geoProject
|
|-geoApp
|
|- migrations
|
|- __init__.py
|- admin.py
|- apps.py
|- models.py
|- tests.py
|- views.py
|
|- geoProject
|
|- __init__.py
|- asgi.py
|- settings.py
|- urls.py
|- wsgi.py
|
|- manage.py
|- src

Bundan sonra yapacağımız ilk iş oluşturduğumuz geoApp uygulamasını ve kullanacağımız teknolojileri geoProject klasörü altında bulunan settings.py dosyasına tanıtmak olacaktır. Bunun için aşağıdaki değişiklikleri gerçekleştirelim.

Önemli not: Aşağıdaki kodlar settings.py dosyasındaki bütün kodları içermemektedir. Değiştirilmesi gereken kodları içermektedir. Eğer kafanızda soru işaretleri var ise github reposunu kontrol etmeyi unutmayın.

#geoProject/settings.py#Javascript üzerinden API adresini fetch ederken CORS problemi yaşamamak için
ALLOWED_HOSTS = ['*']
CORS_ORIGIN_ALLOW_ALL = True
INSTALLED_APPS = [
...
'django.contrib.gis',
'geoApp', #startapp komutu ile oluşturduğumuz uygulama
'rest_framework', #djangorestframework kütüphanesi
'rest_framework_gis', #GeoJSON döndürmek için gerekli kütüphane
'corsheaders', #CORS Hatası almamamız için
]MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', #Bu satırı ekleyin
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
DATABASES = {'default': {
#Buradaki ayarları kendi postgres veritabanı ayarlarınıza göre düzeltin
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'DATABASE_NAME',
'USER': 'USER_NAME',
'PASSWORD': 'PASSWORD',
'HOST': 'localhost',
'PORT': '5432',
}
}

Bu aşamadan sonra oluşturacağımız veri tabanı tablosuna karşılık gelen ORM sınıfını geoApp isimli uygulama klasöründe bulunan models.py dosyası içerisinde tanımlamamız gerekecek. Bunun için öncelikle elimizdeki veriyi inceleyelim. Veriyi incelemek için bir python scripti oluşturacağız ve pandas kütüphanesi ile yüzeysel olarak inceleme gerçekleştireceğiz.

import pandas as pd# İndirdiğimiz dosyanın ismini kolay ulaşabilmek için saglik.csv olarak değiştirdim. Ayrıca Türkçe karakterlerin de gözükmesi için ISO-8859-9 formatında encoding ediyoruz.df = pd.read_csv('saglik.csv', encoding='ISO-8859-9')print(pd.shape)
> (4072, 13) #Verimiz 4072 Satır ve 13 sütundan oluşuyor
#Verimizdeki sütunları görmek için
print(df.columns)
> Index(['ILCE_UAVT', 'ILCE_ADI', 'ADI', 'ALT_KATEGORI', 'ADRES', 'TELEFON','WEBSITESI', 'ACIL_SERVIS', 'YATAK', 'AMBULANS', 'MAHALLE', 'ENLEM','BOYLAM'],
dtype='object')
#Her bir sütun için kayıp verilerin sayısını görmek için
print(df.isna().sum())
> ILCE_UAVT 1
ILCE_ADI 1
ADI 0
ALT_KATEGORI 0
ADRES 8
TELEFON 290
WEBSITESI 2347
ACIL_SERVIS 2229
YATAK 2042
AMBULANS 2907
MAHALLE 7
ENLEM 0
BOYLAM 0

Bu incelemelerin ardından verimiz hakkında bazı kararlar vereceğiz ve karalar sonucu verimizi düzenleyerek postgres’e aktarmaya çalışacağız.

  • Öncelikle baktığımız zaman ILCE_ADI sütununda bir adet boş verimiz bulunuyor fakat buna karşılık enlem ve boylam bilgileri boş gözükmüyor. Buradan hareketle İlce adına nokta konum bilgisi kullanarak ulaşabiliriz fakat öğretici amaçla gerçekleştirdiğimiz bu yazımızı daha fazla uzatmak istemiyoruz. Düzeltmek istersek nasıl düzeltebileceğimiz ile ilgili bilgi sunmak için belirtiyoruz.
  • Kısa bir incelemeyle boş satırları droplamak yerine eğer veri tipimiz text formatına uygun ise verileri ‘BİLGİ YOK’ şeklinde düzenleyeceğiz.
  • YATAK Sayısı integer bir veri tipi olduğu için ‘BİLGİ YOK’ şeklinde tutmak yerine boş değerleri null olarak değiştireceğiz. YATAK SAYISINI text formatına çevirip öyle de tutabilirdik fakat yatak sayısı kullanılarak bir çok analiz ve filtreleme gerçekleştirebiliriz. Örnek olarak istanbuldaki toplam yatak sayısını getirmek isteyebiliriz. Bu yüzden yatak sayısının integer olarak tutulması önemli.
  • ENLEM ve BOYLAM verilerini teker teker sayısal sütunlar olarak tutmak yerine tek bir sütun kullanarak PostGIS’in bize sağlamış olduğu mekansal bir veri tipi olan POINT verisini oluşturacağız.
# Verileri düzenlemek için# ILCE_ADI sütununa karşılık gelen satırı droplamak içindf = df[df['ILCE_ADI'].notna()]# Diğer boş satırları doldurmak için
df = df.fillna(value={'TELEFON': 'BILGI YOK',
'WEBSITESI': 'BILGI YOK',
'ADRES': 'BILGI YOK',
'ACIL_SERVIS': 'BILGI YOK',
'YATAK': 'null',
'AMBULANS': 'BILGI YOK',
'MAHALLE': 'BILGI YOK'})
# Yatak Sayısını veritabanımızda integer olarak tanımlayacağımız için Text olarak 'BİLGİ YOK' demek yerine null olarak dolduruyoruz ve ORM Sınıf modelimizde null değerlere izin veriyoruz.

NOT: Veriler incelendiğinde veritabanı için normalize olmadığı görülmektedir. Örnek olarak ILCE_ADI değerleri için ayrı bir tablonun oluşturulması normalizasyon için ve haliyle hatalı veri girişlerinin önlenmesi için faydalı bir hamle olacaktır. Örnek olarak kadıköy ilçesi için farklı kullanıcılar kadiköy, kadıkoy, kadikoy gibi farklı text değerleri girebilirler. Bu da tek bir ilçe yerine üç farklı ilçe varmış gibi bir sonuç doğuracaktır. Fakat bu yazımızdaki amacımız veritabanı üzerinde kafa yormak olmadığı için kısa bir bilgi olarak bu notu düşmek istedim.

geoApp klasöründe bulunan models.py dosyasını açarak ORM modelimizi oluşturalım.

from django.contrib.gis.db import modelsclass Saglik(models.Model):   

ILCE_ADI = models.CharField(max_length=200)
ADI = models.CharField(max_length=200)
ALT_KATEGORI = models.CharField(max_length=200)
ADRES = models.CharField(max_length=400)
TELEFON = models.CharField(max_length=200)
WEBSITESI = models.CharField(max_length=200)
ACIL_SERVIS = models.CharField(max_length=200)
YATAK = models.IntegerField(null=True)
AMBULANS = models.CharField(max_length=200)
MAHALLE = models.CharField(max_length=200)
POINT = models.GeometryField()
def __str__(self):
return self.ADI

NOT: Oluşturduğumuz mekansal veriye ait mekansal referans bilgisinin (SRID) eklenmediğini belki farketmişsinizdir. Bunun sebebi default değer olarak 4326 olmasıdır. Leaflet ile bu veriyi 4326 srid değerine göre görüntüleyeceğimiz için burada bir müdahale yapmıyoruz. Eğer kullanacağımız nokta verisi farklı bir referans çerçevesinde bulunuyor olsaydı burada bunu belirtmemiz gerekirdi ki kullanacağımız yapı ile arasındaki dönüşümleri postgis aracılığı ile rahatça gerçekleştirebilelim. Aşağıdaki görselde yukarıdaki bilgiler ile kurulmuş veri tabanına ait srid değerini sorgulama kodu gösterilmektedir

Nokta verisine ait srid değeri

Oluşturduğumuz modelin SQL kodlarını üretmek için terminal üzerinden aşağıdaki komutu yazalım.

(env) $ python manage.py makemigrations//Çıktı olarak bize
Migrations for 'geoApp':
geoApp\migrations\0001_initial.py
- Create model Saglik

geoApp klasörünün altında bulunan migrations klasöründe 0001_initial.py dosyası oluştu. Bu dosya içinde nelerin olduğuna bakmak için terminale aşağıdaki komutu yazalım.

(env) $ python manage.py sqlmigrate geoApp 0001// Çıktı olarakBEGIN;
--
-- Create model Hastane
--
CREATE TABLE "geoApp_hastane" ("id" serial NOT NULL PRIMARY KEY, "ILCE_ADI" varchar(200) NOT NULL, "ADI" varchar(200) NOT NULL, "ALT_KATEGORI" varchar(200) NOT NULL, "ADRES" varchar(400) NOT NULL, "TELEFON" varchar(200) NOT NULL, "WEBSITESI" varchar(200) NOT NULL, "ACIL_SERVIS" varchar(200) NOT NULL, "YATAK" integer NULL, "AMBULANS" varchar(200) NOT NULL, "MAHALLE" varchar(200) NOT NULL, "POINT" geometry(GEOMETRY,4326) NOT NULL);
CREATE INDEX "geoApp_hastane_POINT_id" ON "geoApp_hastane" USING GIST ("POINT");
COMMIT;

Yukarıda kodda makemigrations komutu ile geoApp uygulamız altında bulunan models.py dosyasında oluşturduğumuz ORM modelinin sql kodları oluşturulmuş oldu. models.py dosyası altındaki her bir model sınıfı veri tabanımızdaki tablolara karşılık gelmektedir. Şimdi bu kodları postgres veri tabanında çalıştırmak için terminali açarak aşağıdaki komutları girelim.

(env) $ python manage.py migrate// Çıktı olarak Operations to perform:
Apply all migrations: admin, auth, contenttypes, geoApp, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying geoApp.0001_initial... OK
Applying geoApp.0002_auto_20210316_1704... OK
Applying sessions.0001_initial... OK

Şimdi veri tabanızı kontrol ederseniz geoApp_saglik isimli bir tablo oluştuğunu göreceksiniz. Fakat bunların dışında farklı tabloların da oluştuğunu farketmiş olabilirsiniz. Bu tablolar djangonun sağladığı admin sistemi vb. için gerekli veri tabanı tablolarından oluşmaktadır. Projemiz kapsamında bu kısımlara değinmeyeceğiz.

Veri tabanı tablomuzda hazır olduğuna göre sırada veri tabanımıza veri girişi yapmakta. Pandas ile yaptığımız kısa veri incelemesini hatırlıyor iseniz yaklaşık 4000 adet verimiz var ve haliyle bunları tek tek girmek uğraştırıcı ve zahmetli bir iş. Verilerin hızlı bir şekilde girilmesi için projenin başında indirdiğimiz psycopg2 kütüphanesini kullanarak oluşturacağımız bir python scripti ile postgres veritabanımıza bağlanacağız ve gerekli veri giriş işlemlerini otomatikleştireceğiz.

#Oluşturacağınız herhangi bir .py uzantılı dosya içerisineimport pandas as pd
import psycopg2
df = pd.read_csv('saglik.csv', encoding='ISO-8859-9')# Boş verileri doldurmak için işlemler
df = df[df['ILCE_ADI'].notna()]

df = df.fillna(value={'TELEFON': 'BILGI YOK',
'WEBSITESI': 'BILGI YOK',
'ADRES': 'BILGI YOK',
'ACIL_SERVIS': 'BILGI YOK',
'YATAK': 'null',
'AMBULANS': 'BILGI YOK',
'MAHALLE': 'BILGI YOK'})
# Veri tabanına bağlanmak için (Bilgileri kendi veritabanınıza göre doldrunuz
connection = psycopg2.connect('host=hostname dbname=dbname user=user password= password')
# Veritabanı üzerinde işlemler gerçekleştirebilmek için imleç
cursor = connection.cursor()
# Postgres üzerine PostGIS eklentisini kurmak için SQL Kodu çalıştırma
cursor.execute("""
CREATE EXTENSION postgis;
""")
# CSV dosyasındaki verileri veritabanına yazdırmak için döngü içerisinde çalışan SQL Kodu
for index, row in df.iterrows():
cursor.execute(f"""
INSERT INTO "geoApp_saglik"
VALUES ({int(index)+1}, #id için
'{row['ILCE_ADI']}',
'{row['ADI'].replace("'", "")}',
'{row['ALT_KATEGORI']}',
'{row['ADRES']}',
'{row['TELEFON']}',
'{row['WEBSITESI']}',
'{row['ACIL_SERVIS']}',
{row['YATAK']},
'{row['AMBULANS']}',
'{row['MAHALLE']}',
ST_SetSRID(ST_Point({row['BOYLAM']}, {row['ENLEM']}),4326));
""")
# İşlemleri commitlemek için
connection.commit()

Bu işlemin ardından veri tabanızı kontrol ederseniz tüm verilerin geoApp_saglik tablosuna aktarıldığını görecekseniz.

Şimdi hızlıca REST API yazmak için gerekli ayarlamaları yapalım. Öncelikle REST API için gerekli serializer işlemini gerçekleştirmek için geoApp isimli uygulama klasörümüzün altına serializers.py isimli dosya oluşturalım ve içerisine aşağıdaki kodları ekleyelim.

# geoApp/serializers.pyfrom rest_framework_gis.serializers import GeoFeatureModelSerializer
from .models import Saglik
class SaglikSerializer(GeoFeatureModelSerializer):
class Meta:
model = Saglik
geo_field = 'POINT'
fields = '__all__'

Yukarıdaki kodu anlamaya çalışalım. Öncelikle REST API yapısını kurmak için rest_framework_gis kütüphanesini kurduk eğer sadece rest_framework kütüphanesini kullansaydık verilerimizi JSON formatında serialize edecektik. Elimizdeki verilerin mekansal olması nedeniyle geoJSON formatı bizim daha çok işimize yarayacaktır, özellikle leaflet kullanırken. Ardından Serializer sınıfımızı tanımlıyoruz ve bu sınıfın hangi modeli kullanacağını, modelimizde mekansal sütuna karşılık gelen sütunun adını ve API’ya istek yapıldığında hangi sütunların bizlere döndürüleceğini Meta sınıfı içerisinde tanımlıyoruz.

Serializer işlemini tamamladıktan sonra geoApp isimli uygulama klasörü altında bulunan views.py dosyasına girerek aşağıda gösterilen kodları yazıyoruz.

# geoApp/views.pyfrom .serializers import SaglikSerializer
from rest_framework import viewsets
from .models import Saglik
class SaglikViewSet(viewsets.ModelViewSet):
queryset = Saglik.objects.all()
serializer_class = SaglikSerializer

Yukarıdaki kod basitçe veritabanı ve serializer arasındaki ilişkiyi kurarak HTTP Requesti yaptığımızda bize döndüreceği Response için gerekli ayarlamaları yapmamıza olanak sağlıyor. Eğer ileride REST API’ımızı query stringler yardımıyla geliştirmek istersek gerekli ayarlamaları burada yapacağız.

Ardından geoProject isimli proje klasörünün altında bulunan urls.py dosyasına girelim ve uygulamamıza ait url’leri tanıtalım.

# geoProject/urls.pyfrom django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('geoApp.urls'),
]

Admin yolu django’nun sunduğu admin yönetim arayüzüdür. Bu uygulama kapsamında burayı kullanmayacağız.

Ardından geoApp.urls diye tanıttığımız dosya yolunu oluşturalım. Bunun için geoApp isimli uygulama klasörünün içerisine urls.py isimli bir dosya oluşturalım ve içerisine aşağıdaki kodları ekleyelim.

# geoApp/urls.pyfrom django.urls import path, include
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'saglik', views.SaglikViewSet)
urlpatterns = [
path('', include(router.urls)),
]

Yukarıdaki kod bizim http://127.0.0.1:8000/saglik adresine yapacağımız HTTP GET Requestini yönlendirmektedir.

Yukarıdaki tüm ayarlamalarımızı yaptıktan sonra sıra sunucuyu ayağa kaldırma aşamasına geldi bunun için terminale gelerek aşağıdaki komutu çalıştırıyoruz.

(env) $ python manage.py runserver// ÇıktısıWatching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
March 16, 2021 - 20:31:11
Django version 3.1.7, using settings 'geoProject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

Çıktıda gördüğünüz üzere http://127.0.0.1:8000/ adresinde sunucunun ayağa kalktığını söylüyor. Tarayıcınıza http://127.0.0.1:8000/saglik yazarsanız size veri tabanı üzerindeki verilerinize ait bir geoJSON response’u sunacaktır.

HTTP GET Request ile elde edilmiş geoJSON verisinin bir kısmı

Bu aşamaya kadar geldiyseniz tebrik ederim. Birinci kısımda REST API yazmayı tamamladık. Basit haliyle REST API yapısını kurduk. Yukarıdaki görsele bakıldığında Allow kısmında POST requestede izin verdiğini ve herhangi bir authentication da kullanmadığımız için bu API’ın canlıya alınması herkes tarafından veri girişinin sağlanabileceği anlamına gelmektedir. Authentication ve query stringleri ekleyerek daha da geliştirmek tabiki mümkündür fakat bu ve ikinci kısımda bu detaylara inmeyeceğiz. İkinci kısımda Leaflet ile bu verileri nasıl görselleştirebileceğimizi göreceğiz.

İkinci kısımda görüşmek üzere.

Blog yazı serisinin ikinci kısmını okumak için tıklayın.

--

--

Volkan Dağdelen

Geomatics Engineering @ITU | Interested in GIS, Remote Sensing and Backend developing.