Bab 40 — GIS 3: Peta dan Integrasi: PostGIS dan QGIS
Seri I5 — Aplikasi Pertama: Web, Peta, dan Dasbor
Semua skrip bab ini ada di folder kode/bab40/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Dua bab GIS sebelumnya membuat Anda bisa menghitung. Sekarang kita menyajikan dan menyimpan. Peta adalah cara data spasial dibaca orang; basis data spasial adalah gudang yang tahan lama; dan QGIS adalah “dapur peta” yang dipakai banyak instansi. Python bisa duduk di tengah ketiganya.

Peta statis dengan Matplotlib
Peta statis adalah gambar (PNG, PDF) untuk laporan dan buku. GeoPandas menggambar lewat Matplotlib (Bab 25), jadi semua kendali Matplotlib tersedia: judul, legenda, label.
01_peta_statis.py
# Bab 40 - Peta statis dengan GeoPandas dan Matplotlib
from pathlib import Path
import geopandas as gpd
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
petak = gpd.read_file("data/petak.geojson").to_crs(32748)
kolam = gpd.read_file("data/kolam.geojson").to_crs(32748)
kandang = gpd.read_file("data/kandang.geojson").to_crs(32748)
sungai = gpd.read_file("data/sungai.geojson").to_crs(32748)
warna = {"hutan": "#4f8a3b", "kebun": "#d9a441", "sawah": "#e8dc7a"}
fig, ax = plt.subplots(figsize=(7, 5.6), dpi=100)
for sektor, grup in petak.groupby("sektor"):
grup.plot(ax=ax, color=warna[sektor], edgecolor="black", linewidth=0.8)
sungai.plot(ax=ax, color="#3b6a9a", linewidth=2.5)
kolam.plot(ax=ax, color="#1f4e79")
kandang.plot(ax=ax, color="#b04a4a", markersize=60, marker="s")
for _, p in petak.iterrows(): # label kode petak
ax.annotate(p["kode"], (p.geometry.centroid.x, p.geometry.centroid.y), ha="center", weight="bold")
ax.set_title("Peta lokasi usaha (data tiruan)")
ax.set_xlabel("Timur (m, UTM 48S)")
ax.set_ylabel("Utara (m, UTM 48S)")
ax.ticklabel_format(style="plain", useOffset=False)
ax.set_aspect("equal") # 1 m di sumbu X = 1 m di sumbu Y
legenda = [Line2D([0], [0], marker="s", color="w", markerfacecolor=w, markersize=12, label=s)
for s, w in warna.items()]
legenda += [Line2D([0], [0], color="#3b6a9a", lw=2.5, label="sungai"),
Line2D([0], [0], marker="s", color="w", markerfacecolor="#1f4e79", markersize=9, label="kolam"),
Line2D([0], [0], marker="s", color="w", markerfacecolor="#b04a4a", markersize=9, label="kandang")]
ax.legend(handles=legenda, loc="upper left", fontsize=8)
fig.tight_layout()
fig.savefig(tmp / "peta_statis.png")
print("peta tersimpan:", "peta_statis.png", "| ukuran piksel:", fig.get_size_inches() * fig.dpi)peta tersimpan: peta_statis.png | ukuran piksel: [700. 560.]
Hal yang membedakan peta dari grafik biasa:
ax.set_aspect("equal"): 1 meter di sumbu X digambar sama panjang dengan 1 meter di sumbu Y. Tanpa ini, petak persegi tampak gepeng.- Data digambar dalam UTM (meter), jadi sumbu bisa diberi label meter dan jarak terbaca benar.
- Warna dipilih per sektor lewat
groupby, lalu legenda dibuat sendiri dariLine2D.
Gambar 700 × 560 piksel itu tersimpan di _tmp/peta_statis.png. Contoh peta yang sama dengan zona 60 m sudah Anda lihat di Bab 38.
Peta interaktif dengan folium
folium membungkus pustaka JavaScript Leaflet dan menghasilkan berkas HTML yang bisa digeser dan diperbesar di browser, lengkap dengan penanda dan popup.
02_peta_folium.py
# Bab 40 - Peta interaktif dengan folium (hasilnya berkas HTML)
from pathlib import Path
import folium
import geopandas as gpd
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
petak = gpd.read_file("data/petak.geojson") # folium butuh EPSG:4326 (derajat)
kolam = gpd.read_file("data/kolam.geojson")
kandang = gpd.read_file("data/kandang.geojson")
sungai = gpd.read_file("data/sungai.geojson")
pusat = petak.union_all().centroid
peta = folium.Map(location=[pusat.y, pusat.x], zoom_start=16, tiles="OpenStreetMap") # urutan: lintang, bujur
folium.GeoJson(petak, name="petak",
tooltip=folium.GeoJsonTooltip(fields=["kode", "sektor", "komoditas"])).add_to(peta)
folium.GeoJson(sungai, name="sungai", style_function=lambda f: {"color": "#3b6a9a", "weight": 4}).add_to(peta)
for _, k in kolam.iterrows():
folium.Marker([k.geometry.centroid.y, k.geometry.centroid.x],
popup=f"Kolam {k['kode']}: {k['jumlah_ikan']} ikan {k['jenis']}",
icon=folium.Icon(color="blue", icon="tint")).add_to(peta)
for _, k in kandang.iterrows():
folium.Marker([k.geometry.y, k.geometry.x], popup=f"Kandang {k['kode']}: {k['jenis']}",
icon=folium.Icon(color="red")).add_to(peta)
folium.LayerControl().add_to(peta)
peta.save(tmp / "peta_usaha.html")
html = (tmp / "peta_usaha.html").read_text(encoding="utf-8")
print("berkas HTML tersimpan:", "peta_usaha.html")
print("memuat pustaka Leaflet:", "leaflet" in html.lower())
print("jumlah penanda (Marker):", html.count("L.marker("))
print("berisi nama petak 'Meranti':", "Meranti" in html)berkas HTML tersimpan: peta_usaha.html
memuat pustaka Leaflet: True
jumlah penanda (Marker): 7
berisi nama petak 'Meranti': True
- folium memakai EPSG:4326 (derajat), dan urutan lokasinya [lintang, bujur]: kebalikan dari (x, y). Kekeliruan urutan ini membuat peta “mendarat” di laut.
- Skrip memasang lapisan petak (dengan tooltip yang muncul saat kursor mendekat), sungai, tujuh penanda (empat kolam dan tiga kandang), dan kontrol lapisan.
- Bukti yang bisa kita cetak: berkas memuat Leaflet, memuat tujuh
L.marker(, dan memuat nama komoditasMeranti.
Saya tidak membuka peta_usaha.html di browser dalam pembuatan buku ini; yang diperiksa hanya isi berkasnya, seperti pada keluaran di atas. Perhatikan juga bahwa peta dasar (OpenStreetMap) dimuat dari internet saat peta dibuka, jadi tanpa sinyal Anda hanya melihat bentuk petak tanpa latar. Menyalin data sensitif ke berkas HTML berarti data itu ikut terkirim ke siapa pun yang menerima berkasnya.
PostGIS: basis data yang mengerti peta
PostgreSQL adalah basis data relasional (Bab 24); PostGIS adalah ekstensinya yang menambah tipe geometry, ratusan fungsi spasial (ST_Area, ST_Distance, …), dan indeks spasial. Keuntungannya dibanding berkas: banyak pengguna sekaligus, data besar, dan pencarian cepat berdasarkan letak.
Untuk bab ini, pembangun buku menyalakan klaster PostgreSQL sementara (port acak, hanya di komputer ini, dihapus sesudahnya) dan memberi alamatnya lewat variabel DATABASE_URL. Skrip yang memakai ini ditandai @pg. Server PostgreSQL milik penulis tidak disentuh. Di komputer Anda, nilai bawaan postgresql://postgres@localhost:5432/belajar hanyalah contoh: ganti dengan alamat server milik Anda (dan jangan menaruh kata sandi di kode; ambil dari variabel lingkungan, Bab 34).
Membuat tabel dan memuat data
03_postgis_muat_data.py
# Bab 40 - PostGIS: membuat tabel spasial dan memuat data dengan psycopg
import os
import geopandas as gpd
import psycopg
# Di komputer Anda, ganti alamat bawaan ini dengan alamat server PostgreSQL+PostGIS milik Anda
alamat = os.environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432/belajar")
petak = gpd.read_file("data/petak.geojson")
kolam = gpd.read_file("data/kolam.geojson")
sungai = gpd.read_file("data/sungai.geojson")
with psycopg.connect(alamat) as sambungan:
sambungan.execute("CREATE EXTENSION IF NOT EXISTS postgis")
versi = sambungan.execute("SELECT PostGIS_Lib_Version()").fetchone()[0]
print("PostGIS aktif, versi mayor:", versi.split(".")[0])
sambungan.execute("DROP TABLE IF EXISTS petak, kolam, sungai")
sambungan.execute("""CREATE TABLE petak (
kode text PRIMARY KEY, sektor text, komoditas text,
geom geometry(Polygon, 4326))""") # tipe geometri DAN SRID dikunci
sambungan.execute("""CREATE TABLE kolam (
kode text PRIMARY KEY, jenis text, jumlah_ikan int,
geom geometry(Polygon, 4326))""")
sambungan.execute("""CREATE TABLE sungai (
nama text, geom geometry(LineString, 4326))""")
for _, p in petak.iterrows(): # WKT = teks geometri
sambungan.execute("INSERT INTO petak VALUES (%s, %s, %s, ST_GeomFromText(%s, 4326))",
(p["kode"], p["sektor"], p["komoditas"], p.geometry.wkt))
for _, k in kolam.iterrows():
sambungan.execute("INSERT INTO kolam VALUES (%s, %s, %s, ST_GeomFromText(%s, 4326))",
(k["kode"], k["jenis"], int(k["jumlah_ikan"]), k.geometry.wkt))
sambungan.execute("INSERT INTO sungai VALUES (%s, ST_GeomFromText(%s, 4326))",
(sungai["nama"].iloc[0], sungai.geometry.iloc[0].wkt))
# Indeks spasial GiST: membuat pencarian berdasarkan letak cepat
sambungan.execute("CREATE INDEX petak_geom_idx ON petak USING GIST (geom)")
sambungan.execute("CREATE INDEX kolam_geom_idx ON kolam USING GIST (geom)")
for tabel in ("petak", "kolam", "sungai"):
n = sambungan.execute(f"SELECT count(*) FROM {tabel}").fetchone()[0]
print(f"baris di tabel {tabel}:", n)
print("jenis geometri tabel petak:",
sambungan.execute("SELECT DISTINCT ST_GeometryType(geom) FROM petak").fetchone()[0])PostGIS aktif, versi mayor: 3
baris di tabel petak: 6
baris di tabel kolam: 4
baris di tabel sungai: 1
jenis geometri tabel petak: ST_Polygon
CREATE EXTENSION IF NOT EXISTS postgismengaktifkan PostGIS di basis data (butuh hak yang cukup di server Anda).geometry(Polygon, 4326)mengunci jenis bentuk dan SRID (nomor CRS). Memasukkan jenis lain akan ditolak: pengaman yang tidak ada pada berkas.- Bentuk dikirim sebagai WKT (Well-Known Text), teks seperti
POLYGON((...)), dan diubah olehST_GeomFromText. - Indeks GiST membuat pencarian berdasarkan letak cepat. Hasil: 6 petak, 4 kolam, 1 sungai.
Query spasial
04_postgis_query_spasial.py
# Bab 40 - Query spasial di PostGIS (memakai tabel dari skrip 03)
import os
import psycopg
alamat = os.environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432/belajar")
with psycopg.connect(alamat) as sambungan:
# Luas: tipe geography menghitung di bumi bulat, hasilnya meter persegi
print("luas petak (ha):")
for kode, ha in sambungan.execute(
"SELECT kode, round((ST_Area(geom::geography) / 10000)::numeric, 2) FROM petak ORDER BY kode"):
print(f" {kode}: {ha}")
# Petak dalam jarak 60 m dari sungai: ST_DWithin pada tipe geography (jarak dalam meter)
print("\npetak dalam 60 m dari sungai:")
for (kode,) in sambungan.execute(
"SELECT p.kode FROM petak p, sungai s "
"WHERE ST_DWithin(p.geom::geography, s.geom::geography, 60) ORDER BY p.kode"):
print(" ", kode)
# Kolam terdekat ke titik gudang (operator <-> = urut menurut jarak, memakai indeks)
print("\ntiga kolam terdekat ke titik gudang:")
gudang = "ST_SetSRID(ST_MakePoint(106.8140, -6.9950), 4326)"
for kode, jarak in sambungan.execute(
f"SELECT kode, round(ST_Distance(geom::geography, {gudang}::geography)::numeric) "
f"FROM kolam ORDER BY geom <-> {gudang} LIMIT 3"):
print(f" kolam {kode}: {jarak} m")
# Menggabungkan dua tabel menurut letak: tabel petak dan kolam yang saling bersinggungan
n = sambungan.execute("SELECT count(*) FROM petak p JOIN kolam k ON ST_Intersects(p.geom, k.geom)").fetchone()[0]
print("\npasangan petak-kolam yang saling tumpang tindih:", n)luas petak (ha):
H1: 9.00
H2: 6.92
K1: 4.00
K2: 3.00
S1: 3.00
S2: 2.25
petak dalam 60 m dari sungai:
K2
S1
S2
tiga kolam terdekat ke titik gudang:
kolam B: 82 m
kolam C: 172 m
kolam A: 274 m
pasangan petak-kolam yang saling tumpang tindih: 0
Perhatikan:
- Memakai
::geographymembuat PostGIS menghitung di bumi bulat dan menjawab dalam meter walau datanya berderajat. H2 muncul 6,92 ha di sini, sedangkan GeoPandas (UTM) menghitung 6,93 ha: dua metode berbeda menghasilkan selisih kecil karena proyeksi UTM sedikit mengubah ukuran. Untuk keperluan lapangan keduanya cukup dekat; untuk keperluan hukum, ikuti ketentuan resminya. ST_DWithin(..., 60)menemukan petak dalam 60 m dari sungai: K2, S1, S2, sama dengan hasil overlay Bab 38.- Operator
<->mengurutkan menurut jarak dan memanfaatkan indeks: tiga kolam terdekat ke titik gudang (titik contoh) adalah B (82 m), C (172 m), A (274 m). - Tidak ada petak yang tumpang tindih dengan kolam (hasil 0).
Jangan menyusun SQL dengan menempel teks pengguna ke dalam string (f"... {isian} ..."): itu membuka celah SQL injection. Untuk nilai, selalu pakai parameter %s seperti pada INSERT di skrip 03. Skrip 04 menempel teks hanya karena isinya konstanta tetap dari kita sendiri, bukan dari pengguna.
PostGIS dan GeoPandas
05_postgis_geopandas.py
# Bab 40 - PostGIS dan GeoPandas: baca query, tulis tabel baru
import os
import geopandas as gpd
from sqlalchemy import create_engine, text
alamat = os.environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432/belajar")
mesin = create_engine(alamat.replace("postgresql://", "postgresql+psycopg://"))
# Baca hasil query langsung menjadi GeoDataFrame
sql = "SELECT kode, sektor, geom FROM petak WHERE sektor <> 'hutan'"
bukan_hutan = gpd.read_postgis(sql, mesin, geom_col="geom")
print(bukan_hutan[["kode", "sektor"]].to_string(index=False))
print("CRS terbaca:", bukan_hutan.crs)
# Tulis GeoDataFrame ke tabel baru (geoalchemy2 dibutuhkan di belakang layar)
kandang = gpd.read_file("data/kandang.geojson")
kandang.to_postgis("kandang", mesin, if_exists="replace", index=False)
with mesin.connect() as k:
print("baris di tabel kandang:", k.execute(text("SELECT count(*) FROM kandang")).scalar())
print("tipe kolom geometri:", k.execute(text(
"SELECT type || ' / SRID ' || srid FROM geometry_columns WHERE f_table_name = 'kandang'")).scalar())kode sektor
K1 kebun
K2 kebun
S1 sawah
S2 sawah
CRS terbaca: EPSG:4326
baris di tabel kandang: 3
tipe kolom geometri: POINT / SRID 4326
read_postgis mengubah hasil query menjadi GeoDataFrame, dengan CRS terbaca otomatis (EPSG:4326). to_postgis menulis GeoDataFrame ke tabel baru dan membuat kolom bertipe geometri (POINT / SRID 4326). Keduanya membutuhkan SQLAlchemy dan, untuk menulis, GeoAlchemy2 (versi 0.20.0 saya pasang lewat alat/pasang.py).
PyQGIS: mengendalikan QGIS dari Python
QGIS adalah program GIS bebas yang sangat populer. Ia punya Python sendiri dan modul bernama PyQGIS (qgis.core, qgis.analysis) untuk mengotomasi pekerjaan: memuat lapisan, memproses, memberi gaya, mencetak peta. Cara pakai: skrip dijalankan oleh Python milik QGIS, bukan Python proyek ini.
Di komputer penulis, QGIS 4.0.2 terpasang di C:\Program Files\QGIS 4.0.2 dengan Python 3.12 miliknya sendiri. Perintah bin\python-qgis.bat menyiapkan semua jalur lalu menjalankan Python itu. Karena itulah dua skrip berikut diberi arahan @python: yang menunjuk ke berkas itu; di komputer Anda jalurnya akan berbeda (atau memakai konsol Python di dalam QGIS).
Pola dasar skrip tanpa antarmuka (sesuai cookbook PyQGIS resmi):
QgsApplication.setPrefixPath("<folder qgis>", True): beri tahu lokasi pemasangan.app = QgsApplication([], False):False= tanpa GUI.app.initQgis(): muat penyedia data.- … kerja Anda …
app.exitQgis(): bersih-bersih.
Tambahan kita: os.environ["QT_QPA_PLATFORM"] = "offscreen" sebelum QgsApplication dibuat, supaya Qt tidak mencari layar.
06_pyqgis_dasar.py
# Bab 40 - PyQGIS tanpa antarmuka: memuat lapisan, membaca atribut
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen" # tanpa jendela (harus sebelum QgsApplication dibuat)
from qgis.core import Qgis, QgsApplication, QgsProject, QgsVectorLayer
QgsApplication.setPrefixPath("C:/Program Files/QGIS 4.0.2/apps/qgis", True)
app = QgsApplication([], False) # False = tanpa GUI
app.initQgis()
print("versi mayor QGIS:", Qgis.version().split(".")[0])
petak = QgsVectorLayer("data/petak.geojson", "petak", "ogr")
print("lapisan valid:", petak.isValid(), "| jumlah fitur:", petak.featureCount())
print("CRS:", petak.crs().authid(), "| jenis geometri:", petak.geometryType().name)
print("kolom:", [f.name() for f in petak.fields()])
for fitur in petak.getFeatures():
if fitur["sektor"] == "sawah":
print("sawah:", fitur["kode"], fitur["komoditas"])
QgsProject.instance().addMapLayer(petak)
print("lapisan di proyek:", [l.name() for l in QgsProject.instance().mapLayers().values()])
app.exitQgis()versi mayor QGIS: 4
lapisan valid: True | jumlah fitur: 6
CRS: EPSG:4326 | jenis geometri: Polygon
kolom: ['kode', 'sektor', 'komoditas']
sawah: S1 padi
sawah: S2 padi
lapisan di proyek: ['petak']
Keluaran membuktikan skrip berjalan di QGIS 4 (versi mayor 4): enam fitur terbaca, CRS EPSG:4326, jenis geometri poligon, dan kolom kode, sektor, komoditas. Sawah S1 dan S2 ditemukan dengan menyaring atribut.
Algoritma Processing, gaya, dan menggambar peta
QGIS punya ratusan algoritma pengolah (Processing), mis. native:buffer dan native:reprojectlayer. Dari skrip, panggil dengan processing.run(nama, parameter).
07_pyqgis_proses_dan_cetak.py
# Bab 40 - PyQGIS: algoritma Processing, memberi gaya, menggambar peta ke PNG
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
import sys
from pathlib import Path
from qgis.core import (QgsApplication, QgsCategorizedSymbolRenderer, QgsCoordinateReferenceSystem,
QgsMapRendererSequentialJob, QgsMapSettings, QgsProject, QgsRendererCategory,
QgsSymbol, QgsVectorLayer)
from qgis.PyQt.QtCore import QSize
from qgis.PyQt.QtGui import QColor
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
QgsApplication.setPrefixPath("C:/Program Files/QGIS 4.0.2/apps/qgis", True)
app = QgsApplication([], False)
app.initQgis()
# Processing: modul `processing` ada di folder plugin QGIS, jadi jalurnya ditambahkan dulu
sys.path.append("C:/Program Files/QGIS 4.0.2/apps/qgis/python/plugins")
import processing
from processing.core.Processing import Processing
from qgis.analysis import QgsNativeAlgorithms
QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())
Processing.initialize()
petak = QgsVectorLayer("data/petak.geojson", "petak", "ogr")
sungai = QgsVectorLayer("data/sungai.geojson", "sungai", "ogr")
datar = processing.run("native:reprojectlayer", {
"INPUT": petak, "TARGET_CRS": QgsCoordinateReferenceSystem("EPSG:32748"), "OUTPUT": "memory:"})["OUTPUT"]
print("luas petak di UTM (m2):", [round(f.geometry().area()) for f in datar.getFeatures()])
sungai_utm = processing.run("native:reprojectlayer", {
"INPUT": sungai, "TARGET_CRS": QgsCoordinateReferenceSystem("EPSG:32748"), "OUTPUT": "memory:"})["OUTPUT"]
zona = processing.run("native:buffer", {"INPUT": sungai_utm, "DISTANCE": 60, "SEGMENTS": 8, "OUTPUT": "memory:"})["OUTPUT"]
print("jumlah fitur zona penyangga:", zona.featureCount(), "| luas (ha):",
round(next(zona.getFeatures()).geometry().area() / 10_000, 2))
# Gaya: warna berdasarkan kolom sektor
kategori = []
for nilai, warna in [("hutan", "#4f8a3b"), ("kebun", "#d9a441"), ("sawah", "#e8dc7a")]:
simbol = QgsSymbol.defaultSymbol(datar.geometryType())
simbol.setColor(QColor(warna))
kategori.append(QgsRendererCategory(nilai, simbol, nilai))
datar.setRenderer(QgsCategorizedSymbolRenderer("sektor", kategori))
zona.renderer().symbol().setColor(QColor(59, 106, 154, 90)) # biru transparan
# Menggambar peta ke gambar PNG
pengaturan = QgsMapSettings()
pengaturan.setLayers([zona, datar])
pengaturan.setDestinationCrs(datar.crs())
pengaturan.setExtent(zona.extent())
pengaturan.setOutputSize(QSize(700, 450))
pengaturan.setBackgroundColor(QColor("white"))
tugas = QgsMapRendererSequentialJob(pengaturan)
tugas.start()
tugas.waitForFinished()
gambar = tugas.renderedImage()
print("ukuran gambar:", gambar.width(), "x", gambar.height(), "| tersimpan:", gambar.save(str(tmp / "peta_qgis.png")))
proyek = QgsProject.instance()
proyek.setCrs(datar.crs())
proyek.addMapLayers([zona, datar])
print("proyek QGIS tersimpan:", proyek.write(str(tmp / "usaha.qgz")))
app.exitQgis()luas petak di UTM (m2): [90001, 69252, 39999, 30000, 29999, 22500]
jumlah fitur zona penyangga: 1 | luas (ha): 13.23
ukuran gambar: 700 x 450 | tersimpan: True
proyek QGIS tersimpan: True
Luas petak setelah diubah ke UTM (90001, 69252, 39999, … m²) cocok dengan hitungan GeoPandas. Zona penyangga 60 m berluas 13,23 ha (GeoPandas 13,24 ha: beda sedikit karena banyaknya segmen pada ujung buffer). Sesudah itu skrip memberi gaya per kategori (warna menurut kolom sektor), menggambar peta 700 × 450 piksel ke PNG, dan menyimpan proyek QGIS (.qgz) yang bisa dibuka di QGIS sungguhan.
Karena modul processing ada di folder plugin QGIS, skrip menambahkan .../apps/qgis/python/plugins ke sys.path dan mendaftarkan QgsNativeAlgorithms sebelum processing.run. Tanpa dua langkah itu, algoritma tidak ditemukan.
QGIS 4 berbeda dari QGIS 3
QGIS 4.0 (dirilis Maret 2026) pindah dari Qt5/PyQt5 ke Qt6/PyQt6. Akibatnya banyak kode PyQGIS lama perlu disesuaikan. Dua perubahan yang paling sering menabrak:
- Enum Qt harus ditulis lengkap:
Qt.AlignLeftmenjadiQt.AlignmentFlag.AlignLeft. - Tipe kolom memakai
QMetaType.Type(bukanQVariant) padaQgsField.
09_pyqgis4_beda_dengan_qgis3.py
# Bab 40 - QGIS 4 (Qt6): enum Qt harus ditulis lengkap, tipe kolom memakai QMetaType
import os
os.environ["QT_QPA_PLATFORM"] = "offscreen"
from qgis.core import Qgis, QgsField
from qgis.PyQt.QtCore import QMetaType, Qt
# Gaya QGIS 3 (PyQt5): Qt.AlignLeft. Di QGIS 4 (PyQt6) nama pendek itu sudah tidak ada.
try:
Qt.AlignLeft
except AttributeError as galat:
print("gaya lama gagal ->", galat)
print("gaya baru Qt.AlignmentFlag.AlignLeft ->", int(Qt.AlignmentFlag.AlignLeft))
# Tipe kolom: QMetaType.Type menggantikan QVariant pada QgsField
kolom = QgsField("jumlah_ikan", QMetaType.Type.Int)
print("nama kolom:", kolom.name(), "| tipe:", kolom.type())
# Enum milik QGIS sendiri juga dianjurkan ditulis lengkap
print("jenis geometri poligon:", Qgis.GeometryType.Polygon.name)gaya lama gagal -> type object 'Qt' has no attribute 'AlignLeft'
gaya baru Qt.AlignmentFlag.AlignLeft -> 1
nama kolom: jumlah_ikan | tipe: Type.Int
jenis geometri poligon: Polygon
Yang saya buktikan di QGIS 4.0.2 komputer ini: gaya lama Qt.AlignLeft gagal dengan AttributeError, gaya baru berhasil, dan QgsField dengan QMetaType.Type.Int bekerja. Sebagian nama QGIS lama masih berfungsi lewat lapisan kompatibilitas (saya mencoba QgsWkbTypes.PolygonGeometry dan QgsLayoutExporter.Success, keduanya masih menjawab), tetapi bentuk lengkap seperti Qgis.GeometryType.Polygon lebih aman ke depan. Daftar lengkap perubahan ada di panduan kompatibilitas Qt6 di situs QGIS; saya tidak membaca seluruhnya, jadi rujuklah panduan itu untuk kode Anda sendiri.
Dua temuan dari percobaan di komputer penulis: (1) layout cetak (QgsPrintLayout) bisa diekspor ke PNG tanpa antarmuka, tetapi teks pada label tampil sebagai kotak hitam pada mode offscreen (kemungkinan karena tidak ada font yang dimuat; penyebab pastinya belum saya periksa). Karena itu skrip bab ini memakai QgsMapRendererSequentialJob untuk menggambar peta, bukan layout bertulisan. (2) Ekspor layout memancarkan pesan GDAL “ERROR 6” di keluaran walau berhasil; saya tidak memakainya agar keluaran bersih.
Alur kerja Python dan QGIS
| Kebutuhan | Alat yang cocok |
|---|---|
| Hitungan dan analisis berulang | GeoPandas / Rasterio (Python proyek Anda) |
| Penyimpanan bersama, banyak pengguna | PostGIS |
| Memeriksa dan menyunting peta dengan mata dan tikus | QGIS (antarmuka) |
| Mengotomasi langkah QGIS (cetak 20 peta serupa) | PyQGIS |
| Peta untuk laporan atau buku | Matplotlib |
| Peta untuk dibagikan di web | folium |
Pola kerja yang umum: data masuk ke PostGIS atau GeoPackage; analis memeriksanya di QGIS; skrip GeoPandas menghitung ringkasannya; dan PyQGIS atau Matplotlib mencetak peta mingguan.
Proyek Bersambung I5: data lokasi usaha di dua tempat
Lima lapisan lokasi (petak, kolam, kandang, sungai, dusun) kita simpan ke PostGIS (untuk dipakai bersama dan nanti dibaca API) dan ke GeoPackage (satu berkas yang bisa dikirim lewat surel dan dibuka di QGIS), lalu dicek bahwa keduanya berisi hal yang sama.
08_proyek_dua_penyimpanan.py
# Bab 40 - Proyek bersambung I5: data lokasi disimpan di PostGIS dan GeoPackage
import os
from pathlib import Path
import geopandas as gpd
import pandas as pd
from sqlalchemy import create_engine
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
alamat = os.environ.get("DATABASE_URL", "postgresql://postgres@localhost:5432/belajar")
mesin = create_engine(alamat.replace("postgresql://", "postgresql+psycopg://"))
lapisan = ["petak", "kolam", "kandang", "sungai", "dusun"]
gpkg = tmp / "usaha.gpkg"
gpkg.unlink(missing_ok=True)
for nama in lapisan:
gdf = gpd.read_file(f"data/{nama}.geojson")
gdf.to_postgis(f"usaha_{nama}", mesin, if_exists="replace", index=False) # server
gdf.to_file(gpkg, layer=nama, driver="GPKG") # berkas portabel
print("lapisan PostGIS GeoPackage")
for nama in lapisan:
a = len(gpd.read_postgis(f"SELECT * FROM usaha_{nama}", mesin, geom_col="geometry"))
b = len(gpd.read_file(gpkg, layer=nama))
print(f"{nama:8s} {a:7d} {b:11d}")
# Luas per sektor: dihitung di server PostGIS dan di Python dari GeoPackage; harus sama
dari_server = pd.read_sql(
"SELECT sektor, round((sum(ST_Area(geometry::geography)) / 10000)::numeric, 1) AS ha "
"FROM usaha_petak GROUP BY sektor ORDER BY sektor", mesin)
print("\nluas per sektor (ha) dari PostGIS:")
print(dari_server.to_string(index=False))
petak = gpd.read_file(gpkg, layer="petak").to_crs(32748)
print("\nluas per sektor (ha) dari GeoPackage:")
print(petak.assign(ha=petak.area / 10_000).groupby("sektor")["ha"].sum().round(1).to_string())lapisan PostGIS GeoPackage
petak 6 6
kolam 4 4
kandang 3 3
sungai 1 1
dusun 2 2
luas per sektor (ha) dari PostGIS:
sektor ha
hutan 15.9
kebun 7.0
sawah 5.2
luas per sektor (ha) dari GeoPackage:
sektor
hutan 15.9
kebun 7.0
sawah 5.2
Lima lapisan, jumlah baris sama di kedua tempat, dan luas per sektor cocok: hutan 15,9 ha, kebun 7,0 ha, sawah 5,2 ha (ketiganya dibulatkan satu desimal; sawah 5,25 menjadi 5,2 karena pembulatan Python). Tabel di PostGIS diberi awalan usaha_ supaya tidak bertabrakan dengan tabel skrip 03. Itu mengakhiri bagian peta dari proyek Seri I5: data lokasi sudah rapi dan tersimpan.
Latihan
- Di
01_peta_statis.py, tambah garis skala sederhana (sebuah garis 100 m dengan teks “100 m”) di pojok kanan bawah peta. - Di
02_peta_folium.py, gantitiles="OpenStreetMap"menjaditiles="CartoDB positron"dan lihat perbedaan isi berkas. - Di
04_postgis_query_spasial.py, tambah query yang menghitung jumlah ikan per dusun memakaiST_Withinantara titik pusat kolam (ST_Centroid) dan poligon dusun. (Tabel dusun belum dimuat: tambahkan di skrip 03.) - Tantangan: di
07_pyqgis_proses_dan_cetak.py, tambah algoritmanative:intersectionantara petak dan zona, lalu cetak luas potongan tiap petak, dan cocokkan dengan Bab 38.
Ringkasan Bab
- Matplotlib untuk peta statis (jangan lupa
set_aspect("equal")); folium untuk peta interaktif (HTML, urutan [lintang, bujur]). - PostGIS menambah tipe
geometry, fungsiST_*, dan indeks spasial pada PostgreSQL; kirim nilai lewat parameter, bukan tempelan teks. ::geographymenghitung di bumi bulat dalam meter; hasilnya dapat sedikit berbeda dari hitungan UTM.- PyQGIS berjalan di Python milik QGIS; skrip tanpa antarmuka butuh
setPrefixPath,QgsApplication([], False),initQgis, danexitQgis. - QGIS 4 memakai Qt6: tulis enum Qt lengkap dan pakai
QMetaType.Type. - Penyimpanan ganda: PostGIS untuk bersama, GeoPackage untuk portabel.
Selanjutnya, di Bab 41: otomasi tugas dan web scraping.