BAB 17: Dari Data ke Lembar Peta: Mewarnai, Memberi Label, dan Mencetak dengan Skrip
Studi kasus: "Peta volume kayu untuk rapat besok" #
Kepala Seksi minta peta volume kayu per petak untuk rapat besok pagi. Anda bisa mengklik menu satu per satu: warna, label, layout, ekspor. Hasilnya bagus. Tetapi bulan depan data volume berubah, dan semua klik harus diulang.
Dengan skrip, Anda menyusun peta itu sekali. Bulan depan Anda cukup menjalankannya lagi, dan peta baru jadi dalam hitungan detik.
Konsep: peta jadi dalam tiga kalimat #
Peta yang enak dibaca dirakit bertahap, seperti menyusun poster. Layer adalah bahan, simbologi adalah cat, label adalah tulisan, layout adalah lembar posternya, dan ekspor adalah mencetaknya. Skrip PyQGIS mengerjakan tiap tahap itu dengan beberapa baris.

Lima istilah yang perlu Anda kenal sekarang:
- Simbologi (symbology): aturan warna dan bentuk gambar sebuah layer di peta.
- Penyaji (renderer): objek QGIS yang memegang aturan simbologi satu layer.
- Simbol: satu tampilan, misalnya satu isian poligon cokelat dengan garis tepi tipis.
- Label: tulisan di atas fitur peta, misalnya kode petak.
- Layout: lembar peta untuk dicetak, berisi peta, judul, legenda, skala, dan panah utara.
Sebelum mulai: urutan menjalankan dan bahan #
Skrip di bab ini tidak berdiri sendiri semua. Beberapa skrip mengambil layer yang dibuat skrip sebelumnya, lewat nama layer di proyek. Karena itu urutannya penting. Tabel berikut merangkumnya.
| Skrip | Berkas | Syarat sebelum dijalankan |
|---|---|---|
| 17.1 | skrip_4_1_kategori.py | Tidak ada. Memuat layer sendiri. |
| 17.2 | skrip_4_2_klasifikasi.py | Jalankan 17.1 lebih dulu, di proyek yang sama. |
| 17.3 | skrip_4_2b_raster.py | Tidak ada, tetapi sebaiknya di proyek terpisah (lihat Jebakan 17.3). |
| 17.4 | skrip_4_3_label.py | Jalankan 17.1 lebih dulu. |
| 17.5 | skrip_4_4_layout.py | Jalankan 17.1, 17.2, dan 17.4 di proyek yang sama. Jangan jalankan 17.3. |
| 17.6 | skrip_4_5_ekspor.py | Jalankan 17.5 lebih dulu. |
| 17.7 | skrip_4_6_jembatan.py | Tidak ada, tetapi mengosongkan proyek. Jalankan di proyek kosong. |
Menjalankan. Buka Konsol Python QGIS atau Editor Skrip, seperti yang Anda siapkan di Bab 3. Tempel isi skrip, lalu jalankan. Semua skrip memakai satu folder kerja, yaitu isi variabel FOLDER.
Bahan. Buat satu folder kerja, misalnya D:/KPH_Contoh. Salin lima berkas ke dalamnya.
- Dari
paket-b4:Petak_KPH.gpkg(layerPetak_KPH, 16 petak) danSungai_Jalan.gpkg(layerSungaidanJalan). - Dari
paket-data:Hotspot_Nasional.gpkg(12 titik panas) danDTM_Drone.tif(peta tinggi). - Dari
paket-b1:Hotspot_derajat.gpkg. Hanya Skrip 17.7 yang memakainya.
Di setiap skrip, FOLDER tertulis r"D:/KPH_Contoh", yaitu jalur komputer penulis. Ganti dengan jalur folder Anda. Gunakan garis miring biasa /, seperti dijelaskan di Bab 8.
Pengujian. Semua hasil di bab ini dijalankan di QGIS 4.0.2 tanpa jendela utama (mode tanpa tampilan). Urutan uji: 17.1, 17.2, 17.4, 17.5, 17.6 dalam satu proyek. Skrip 17.3 dan 17.7 diuji sendiri-sendiri. Hasil tampilan visual dibaca dari berkas PDF dan PNG yang dihasilkan.
Skrip 17.1: Muat layer dan beri warna per jenis #
Berkas skrip_4_1_kategori.py sama dengan yang dipakai di Buku B4. Di dalam berkasnya tertulis "Skrip 4.1" mengikuti penomoran buku itu.
Tujuan. Memuat empat layer KPH Contoh, mewarnai petak menurut jenis tanaman, lalu memberi gaya pada sungai, jalan, dan titik panas.
Bahan. Folder kerja berisi Petak_KPH.gpkg, Sungai_Jalan.gpkg, dan Hotspot_Nasional.gpkg. Skrip ini memuat layer sendiri, jadi tidak butuh skrip lain lebih dulu. Proyek yang sedang terbuka langsung berubah.

Gambar di atas menunjukkan dua cara mewarnai. Skrip ini memakai cara kiri: Categorized, satu nilai teks satu warna. Cara kanan, Graduated, dipakai di Skrip 17.2.
Skrip ini bekerja dalam delapan tahap. Gambar berikut menunjukkan urutannya, lengkap dengan nomor baris.

Skrip utuh:
# Skrip 4.1: memuat empat layer KPH Contoh, lalu menata simbologinya.
# Petak diwarnai per jenis tanaman (Categorized); garis dan titik memakai Single Symbol.
from qgis.core import (QgsProject, QgsVectorLayer, QgsFillSymbol, QgsLineSymbol, QgsMarkerSymbol,
QgsRendererCategory, QgsCategorizedSymbolRenderer, QgsSingleSymbolRenderer,
QgsCoordinateReferenceSystem)
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
proyek = QgsProject.instance()
proyek.setCrs(QgsCoordinateReferenceSystem("EPSG:32749")) # CRS proyek: UTM 49S (meter)
def muat(berkas, lapisan, nama):
layer = QgsVectorLayer("%s/%s|layername=%s" % (FOLDER, berkas, lapisan), nama, "ogr")
assert layer.isValid(), "Layer tidak terbaca: " + berkas
proyek.addMapLayer(layer) # layer yang ditambah belakangan berada di atas
return layer
petak = muat("Petak_KPH.gpkg", "Petak_KPH", "Petak_KPH")
sungai = muat("Sungai_Jalan.gpkg", "Sungai", "Sungai")
jalan = muat("Sungai_Jalan.gpkg", "Jalan", "Jalan")
titik = muat("Hotspot_Nasional.gpkg", "Hotspot_Nasional", "Hotspot_Nasional")
# Petak: satu warna untuk tiap jenis
warna = {"Jati": "#8C6D31", "Mahoni": "#B5523B", "Sengon": "#7FB069",
"Akasia": "#D9A400", "Agroforestri": "#4F9DA6"}
kategori = []
for nama, hexa in warna.items():
simbol = QgsFillSymbol.createSimple({"color": hexa, "outline_color": "#404040", "outline_width": "0.3"})
kategori.append(QgsRendererCategory(nama, simbol, nama))
petak.setRenderer(QgsCategorizedSymbolRenderer("Jenis", kategori))
# Sungai: garis biru; Jalan: garis cokelat putus-putus; Titik panas: lingkaran merah
sungai.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple(
{"line_color": "#2B6CB0", "line_width": "0.8"})))
jalan.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple(
{"line_color": "#5A4632", "line_width": "0.5", "line_style": "dash"})))
titik.setRenderer(QgsSingleSymbolRenderer(QgsMarkerSymbol.createSimple(
{"name": "circle", "color": "#E03C31", "outline_color": "#FFFFFF", "size": "2.6"})))
for layer in (petak, sungai, jalan, titik):
layer.triggerRepaint()
print("Kategori dipasang:", [k.label() for k in petak.renderer().categories()])
print("Layer di proyek:", [l.name() for l in proyek.mapLayers().values()])Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-2 | # Skrip 4.1 ... | Komentar. Python melewatinya. Isinya menyebut bahwa skrip memuat empat layer: petak, sungai, jalan, dan titik panas. |
| 3-5 | from qgis.core import (...) | Panggil sembilan perkakas QGIS. Tanda kurung membolehkan daftar nama disambung ke beberapa baris. |
| 7 | FOLDER = r"D:/KPH_Contoh" | Isi kotak FOLDER dengan lokasi folder kerja. Huruf r membuat teks dibaca apa adanya. Bagian ini yang Anda ganti. |
| 8 | proyek = QgsProject.instance() | Ambil proyek QGIS yang sedang terbuka, lalu simpan di kotak proyek. |
| 9 | proyek.setCrs(...) | Atur sistem koordinat proyek ke EPSG:32749, yaitu UTM zona 49 Selatan dengan satuan meter. |
| 11-15 | def muat(berkas, lapisan, nama): | Buat fungsi bernama muat, yaitu resep yang bisa dipakai berkali-kali dengan tiga isian (dibahas di Bab 7). Isinya membuat layer, memeriksanya, menambahkannya ke proyek, lalu mengembalikannya. |
| 12 | layer = QgsVectorLayer("%s/%s... | Susun alamat data dari tiga isian: folder, berkas, lalu garis tegak dan nama layer di dalam berkas. Tanda %s diganti isian berurutan oleh tanda % di ujung (format teks, Bab 9). Isian terakhir, "ogr", adalah pembaca data vektor. |
| 13 | assert layer.isValid(), "Layer tidak terbaca: " + berkas | Pemeriksa (assert, Bab 10). Fungsi isValid() bernilai True bila data berhasil dibaca. Bila salah, skrip berhenti dengan AssertionError dan pesan Layer tidak terbaca: diikuti nama berkasnya. Tanpa baris ini, jalur yang salah menghasilkan layer kosong tanpa peringatan. |
| 14 | proyek.addMapLayer(layer) | Tambahkan layer ke proyek. Layer yang ditambah belakangan berada di atas. |
| 17-20 | petak = muat(...) dan tiga baris di bawahnya | Panggil muat empat kali. Hasilnya masuk ke kotak petak, sungai, jalan, dan titik. |
| 23-24 | warna = {...} | Kamus (Bab 5): pasangan nama jenis dan kode warna heksadesimal. Contoh "#8C6D31" adalah cokelat. Nama jenis harus persis sama dengan isi kolom Jenis. |
| 25 | kategori = [] | Siapkan daftar kosong untuk menampung kategori. |
| 26 | for nama, hexa in warna.items(): | Ulangi untuk tiap pasangan di kamus. Di tiap putaran, nama berisi nama jenis dan hexa berisi kode warnanya. |
| 27 | simbol = QgsFillSymbol.createSimple({...}) | Buat simbol isian poligon. Kamus kecil di dalamnya mengatur warna isi, warna garis tepi, dan tebal garis tepi (dalam milimeter). |
| 28 | kategori.append(QgsRendererCategory(...)) | Buat satu kategori dari tiga hal: nilai yang dicari, simbolnya, dan tulisan di legenda. Lalu tambahkan ke daftar. |
| 29 | petak.setRenderer(QgsCategorizedSymbolRenderer("Jenis", kategori)) | Pasang penyaji berkategori pada petak. Penyaji melihat kolom Jenis, lalu memilih simbol menurut isinya. |
| 32-33 | sungai.setRenderer(...) | Beri sungai satu simbol garis biru setebal 0,8 mm. QgsSingleSymbolRenderer berarti satu simbol untuk semua fitur. |
| 34-35 | jalan.setRenderer(...) | Beri jalan garis cokelat putus-putus. Pengaturan "line_style": "dash" membuatnya putus-putus. |
| 36-37 | titik.setRenderer(...) | Beri titik panas simbol lingkaran merah dengan garis tepi putih, ukuran 2,6. |
| 39-40 | for layer in (...): layer.triggerRepaint() | Ulangi untuk keempat layer. Minta QGIS menggambar ulang agar warna baru terlihat. |
| 41 | print("Kategori dipasang:", [...]) | Tampilkan daftar tulisan kategori. Tanda kurung siku dengan for di dalamnya adalah list comprehension (Bab 6). |
| 42 | print("Layer di proyek:", [...]) | Tampilkan nama semua layer di proyek. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Kategori dipasang: ['Jati', 'Mahoni', 'Sengon', 'Akasia', 'Agroforestri']
Layer di proyek: ['Hotspot_Nasional', 'Jalan', 'Petak_KPH', 'Sungai']Urutan nama pada baris kedua mengikuti catatan internal QGIS, bukan urutan di panel Layers. Di peta (saya merendernya sendiri untuk memastikan), petak tampil dalam lima warna menurut jenis, sungai berupa garis biru, jalan putus-putus cokelat, dan 12 titik merah.
Bagian yang boleh diubah.
- Baris 7, lokasi data. Ganti jalur dengan folder kerja Anda.
- Baris 23-24, warna. Ganti kode heksadesimal untuk mengubah warna jenis tertentu. Nama jenisnya tidak boleh diubah.
- Baris 27,
outline_width. Angka"0.3"adalah tebal garis tepi petak. Ganti menjadi"0.1"untuk garis lebih tipis. - Baris 33 dan 35,
line_width. Ganti angkanya untuk mengatur tebal sungai dan jalan. - Baris 35,
line_style. Ganti"dash"menjadi"solid"untuk garis penuh. - Baris 37,
size. Angka"2.6"adalah ukuran titik panas. - Baris 29,
"Jenis". Ganti dengan kolom teks lain untuk mewarnai menurut kolom itu.
Jebakan.
- Jalur salah menghentikan skrip. Bila
FOLDERmenunjuk folder yang tidak ada, baris 13 menghentikan skrip denganAssertionError: Layer tidak terbaca:diikuti nama berkas pertama yang gagal. Pesan ini menolong, karena Anda tahu penyebabnya sebelum melihat peta kosong. Tanpa baris 13, skrip jalan terus, keempat layer kosong, dan peta tidak menampilkan apa pun. Bila pesan itu muncul, periksa baris 7 dan nama berkas. - Jenis yang tidak ada di kamus. Misalnya Anda menghapus
"Akasia"dari kamuswarna. Petak berjenis Akasia lalu tidak punya kategori. Uji menunjukkancategoryIndexForValue("Akasia")bernilai-1, artinya tidak ada kategori yang cocok. Petak itu tidak tergambar di peta: pada hasil render saya, petak B03 (Akasia) tampil putih tanpa isian, dan legenda tidak memuat Akasia. - Komentar bisa tertinggal dari isi. Python tidak memeriksa komentar. Bila Anda menambah layer, ubah juga komentar di baris 1. Saat membaca skrip orang lain, percayai kodenya, bukan komentarnya.
- Skrip berikutnya menimpa warna. Skrip 17.2 memasang penyaji baru pada layer
Petak_KPH. Warna per jenis dari skrip ini lalu hilang, diganti warna menurut volume.
Latihan variasi.
- Ganti warna Jati menjadi hijau tua
#2E7D32. - Buat sungai lebih tebal, yaitu 1,2.
Contoh jawaban, hanya bagian yang berubah:
warna = {"Jati": "#2E7D32", "Mahoni": "#B5523B", "Sengon": "#7FB069",
"Akasia": "#D9A400", "Agroforestri": "#4F9DA6"}sungai.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple(
{"line_color": "#2B6CB0", "line_width": "1.2"})))Saya menambah satu baris print untuk memeriksa. Hasilnya Warna Jati: #2e7d32 | tebal sungai: 1.2.
Skrip 17.2: Klasifikasi angka menjadi kelas warna #
Berkas skrip_4_2_klasifikasi.py. Di dalam berkasnya tertulis "Skrip 4.2".
Tujuan. Mewarnai petak menurut Volume_m3ha dalam empat kelas, membandingkan tiga metode pembagian kelas, dan menampilkan petak yang belum diukur sebagai layer abu-abu tersendiri.
Bahan. Jalankan Skrip 17.1 lebih dulu, di proyek yang sama, agar layer Petak_KPH sudah ada. Folder kerja juga memuat Petak_KPH.gpkg, karena baris 43 membukanya sekali lagi. Dari 16 petak, 15 punya volume dan satu petak, G08, kosong.
Angka seperti volume tidak cocok diwarnai satu warna per nilai, karena hampir semua nilainya berbeda. Solusinya membagi angka menjadi kelas (rentang), lalu memberi satu warna untuk tiap kelas. Penyaji untuk ini bernama Graduated.
Bayangkan Anda membagi murid ke dalam kelas. Ada tiga cara membaginya, dan hasilnya berbeda.
- Equal Interval: tiap kelas selebar sama. Rentang nilai dipotong rata, tanpa melihat banyaknya petak.
- Quantile (di QGIS bernama Equal Count): tiap kelas berisi petak yang jumlahnya hampir sama.
- Natural Breaks (juga disebut Jenks): batas kelas dipasang di celah besar antar kelompok nilai.

Gambar di atas memakai 15 nilai volume yang nyata. Perhatikan Equal Interval: kelas keduanya kosong, karena tidak ada petak bervolume antara 98 dan 150. Skrip ini menghitung semua itu.

Skrip utuh:
# Skrip 4.2: simbologi Graduated untuk Volume_m3ha, membandingkan metode klasifikasi,
# dan menangani petak yang nilainya kosong (NULL)
from qgis.core import (QgsProject, QgsStyle, QgsGraduatedSymbolRenderer, QgsClassificationEqualInterval,
QgsClassificationQuantile, QgsClassificationJenks, QgsFillSymbol, QgsVectorLayer,
QgsSingleSymbolRenderer, QgsSymbolLayerUtils)
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
petak = QgsProject.instance().mapLayersByName("Petak_KPH")[0]
# 1. Periksa nilai kosong. Metode Quantile dan Natural Breaks membacanya sebagai nol.
kosong = [f["ID_Petak"] for f in petak.getFeatures() if f["Volume_m3ha"] is None]
print("Petak tanpa nilai:", kosong)
# 2. Saring sementara: hanya petak yang terisi ikut dihitung
petak.setSubsetString('"Volume_m3ha" IS NOT NULL')
def pasang(kelas_metode, jumlah=4):
metode = kelas_metode() # objek metode baru tiap kali dipakai
rend = QgsGraduatedSymbolRenderer("Volume_m3ha")
rend.setSourceSymbol(QgsFillSymbol.createSimple({"outline_color": "#404040", "outline_width": "0.3"}))
rend.setClassificationMethod(metode)
rend.updateClasses(petak, jumlah)
rend.updateColorRamp(QgsStyle.defaultStyle().colorRamp("YlGn")) # skala kuning ke hijau, objek baru tiap kali
return rend
for kelas_metode in (QgsClassificationEqualInterval, QgsClassificationQuantile, QgsClassificationJenks):
rend = pasang(kelas_metode)
print(rend.classificationMethod().name())
for i, r in enumerate(rend.ranges()):
# nilai tepat di batas masuk kelas bawahnya; kelas pertama memuat nilai terkecil
n = sum(1 for f in petak.getFeatures()
if (r.lowerValue() < f["Volume_m3ha"] <= r.upperValue()) or (i == 0 and f["Volume_m3ha"] == r.lowerValue()))
print(" %7.1f - %7.1f %d petak" % (r.lowerValue(), r.upperValue(), n))
# 3. Pakai Quantile (4 kelas) pada layer, lalu lepas saringan
rend = pasang(QgsClassificationQuantile)
petak.setRenderer(rend)
petak.setSubsetString("")
petak.triggerRepaint()
# 4. Petak yang belum diukur ditampilkan sebagai layer tersendiri berwarna abu-abu
belum = QgsVectorLayer(FOLDER + "/Petak_KPH.gpkg|layername=Petak_KPH", "Belum diukur", "ogr")
belum.setSubsetString('"Volume_m3ha" IS NULL')
belum.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
{"color": "#D0D0D0", "outline_color": "#404040", "outline_width": "0.3", "style": "b_diagonal"})))
proyek = QgsProject.instance()
proyek.addMapLayer(belum, False) # False: belum dimasukkan ke panel Layers
akar = proyek.layerTreeRoot()
posisi = akar.children().index(akar.findLayer(petak.id()))
akar.insertLayer(posisi, belum) # tepat di atas Petak_KPH
print("Layer Belum diukur:", belum.featureCount(), "petak")Penjelasan baris demi baris. Bagian yang sama dengan Skrip 17.1 tidak dijelaskan ulang. Baris 3-5 dan baris 7 mengikuti pola yang sama, hanya nama perkakasnya berbeda.
| Baris | Tulisan | Artinya |
|---|---|---|
| 3-5 | from qgis.core import (...) | Panggil sepuluh perkakas. Satu di antaranya, QgsSymbolLayerUtils, tidak dipakai. Impor yang tersisa tidak berpengaruh. |
| 9 | petak = QgsProject.instance().mapLayersByName("Petak_KPH")[0] | Cari layer di proyek menurut namanya. Hasilnya berupa daftar, jadi [0] mengambil yang pertama. |
| 12 | kosong = [f["ID_Petak"] for f in ... if f["Volume_m3ha"] is None] | Kumpulkan kode petak yang volumenya kosong. Nilai kosong di Python bernama None. |
| 13 | print("Petak tanpa nilai:", kosong) | Tampilkan petak yang kosong. |
| 16 | petak.setSubsetString('"Volume_m3ha" IS NOT NULL') | Pasang saringan sementara: layer hanya "melihat" petak yang volumenya terisi. Teks luar memakai kutip tunggal supaya nama kolom di dalamnya boleh memakai kutip ganda. |
| 18 | def pasang(kelas_metode, jumlah=4): | Fungsi dengan dua isian. Isian jumlah punya nilai bawaan 4, jadi boleh tidak diisi (Bab 7). |
| 19 | metode = kelas_metode() | kelas_metode berisi satu "cetakan" metode. Tanda kurung membuat satu objek baru dari cetakan itu. Komentar mengingatkan: objek baru tiap kali. |
| 20 | rend = QgsGraduatedSymbolRenderer("Volume_m3ha") | Buat penyaji bertingkat yang membaca kolom Volume_m3ha. |
| 21 | rend.setSourceSymbol(...) | Tetapkan simbol dasar: hanya garis tepi abu tua. Warna isi akan datang dari skala warna. |
| 22 | rend.setClassificationMethod(metode) | Pilih metode pembagian kelas. |
| 23 | rend.updateClasses(petak, jumlah) | Hitung batas kelas dari data layer, sebanyak jumlah kelas. |
| 24 | rend.updateColorRamp(...colorRamp("YlGn")) | Pasang skala warna YlGn, dari kuning ke hijau. Objek skala warna dibuat baru tiap kali. |
| 25 | return rend | Kembalikan penyaji yang sudah jadi. |
| 27 | for kelas_metode in (A, B, C): | Ulangi tiga kali, sekali untuk tiap metode. Nama metode disimpan di dalam tanda kurung seperti nilai biasa. |
| 28-29 | rend = pasang(kelas_metode), print(...) | Buat penyaji dengan metode itu, lalu tampilkan nama metodenya. |
| 30 | for i, r in enumerate(rend.ranges()): | Ulangi untuk tiap kelas. enumerate menambahkan nomor urut i dari 0 (Bab 6). |
| 32-33 | n = sum(1 for f in ... if (...) or (...)) | Hitung petak di kelas ini. Tiap petak yang memenuhi syarat menyumbang angka 1. Syarat pertama, batas_bawah < nilai <= batas_atas, adalah perbandingan berantai. Syarat kedua membuat nilai terkecil ikut masuk kelas pertama. |
| 34 | print(" %7.1f - %7.1f %d petak" % (...)) | Cetak batas bawah, batas atas, dan jumlah petak. %7.1f berarti angka desimal, satu angka di belakang koma, lebar 7 karakter. |
| 37-38 | rend = pasang(QgsClassificationQuantile), petak.setRenderer(rend) | Pilih Quantile, empat kelas, lalu pasang di layer. |
| 39-40 | petak.setSubsetString(""), petak.triggerRepaint() | Lepas saringan, supaya petak kosong muncul kembali di peta. Lalu gambar ulang. |
| 43 | belum = QgsVectorLayer(FOLDER + "/Petak_KPH.gpkg... | Buka data yang sama sekali lagi sebagai layer baru bernama Belum diukur. |
| 44 | belum.setSubsetString('"Volume_m3ha" IS NULL') | Saringan kebalikannya: hanya petak yang volumenya kosong. |
| 45-46 | belum.setRenderer(...) | Beri simbol isian abu-abu dengan pola garis miring (b_diagonal). |
| 47 | proyek = QgsProject.instance() | Ambil proyek dan simpan di kotak proyek. Baris 9 dan 38 tadi memanggilnya langsung. |
| 48 | proyek.addMapLayer(belum, False) | Daftarkan layer ke proyek. Isian False berarti belum dimasukkan ke panel Layers. |
| 49 | akar = proyek.layerTreeRoot() | Ambil "akar pohon" panel Layers. |
| 50 | posisi = akar.children().index(akar.findLayer(petak.id())) | Cari nomor urut Petak_KPH di panel Layers. |
| 51 | akar.insertLayer(posisi, belum) | Sisipkan Belum diukur pada nomor urut itu, sehingga tepat di atas Petak_KPH. |
| 52 | print("Layer Belum diukur:", ...) | Tampilkan jumlah petak di layer itu. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Petak tanpa nilai: ['G08']
Equal Interval
45.5 - 97.7 8 petak
97.7 - 149.8 0 petak
149.8 - 202.0 4 petak
202.0 - 254.1 3 petak
Equal Count (Quantile)
45.5 - 67.3 4 petak
67.3 - 89.1 4 petak
89.1 - 197.1 3 petak
197.1 - 254.1 4 petak
Natural Breaks (Jenks)
45.5 - 60.4 4 petak
60.4 - 89.1 4 petak
89.1 - 161.4 2 petak
161.4 - 254.1 5 petak
Layer Belum diukur: 1 petakDari hasil ini Anda tahu tiga hal. Pertama, G08 kosong. Kedua, Equal Interval menyisakan satu kelas tanpa petak. Ketiga, setiap metode menghasilkan 15 petak bila dijumlahkan.
Satu aturan batas kelas perlu Anda tahu. Nilai yang tepat berada di batas masuk ke kelas di bawahnya. Contohnya A02 bervolume 89,1. Pada metode Quantile, nilai itu masuk kelas 67,3 sampai 89,1, bukan kelas berikutnya.
Bagian yang boleh diubah.
- Baris 18,
jumlah=4. Ganti angka bawaan untuk mengubah jumlah kelas. Anda juga boleh menulispasang(kelas_metode, 5). - Baris 24,
"YlGn". Ganti dengan nama skala warna lain, misalnya"Blues". Uji:"Blues"berjalan baik dan menghasilkan warna kelas dari#f7fbffsampai#08306b. - Baris 37, metode. Ganti
QgsClassificationQuantiledenganQgsClassificationJenksatauQgsClassificationEqualInterval. - Baris 12, 16, 20, 33, dan 44, nama kolom. Ganti
Volume_m3hadengan kolom angka lain. Ganti di semua baris itu. - Baris 45,
"color": "#D0D0D0". Ganti untuk mengubah warna abu-abu petak kosong.
Jebakan.
- Skrip 17.1 belum dijalankan. Layar menampilkan
IndexError: list index out of rangepada baris 9. Artinya daftar hasil pencarian kosong, karena layerPetak_KPHtidak ada di proyek. Jalankan Skrip 17.1 dulu. - Nilai kosong dibaca sebagai nol. Baris 11 menyatakan bahwa Quantile dan Natural Breaks membaca nilai kosong sebagai nol. Saya mengujinya dengan membuang saringan di baris 16. Hasilnya, batas bawah kelas pertama Quantile menjadi 0,0 dan Natural Breaks menghasilkan kelas
0.0 - 0.0. Karena itu skrip menyaring dulu. - Salah ketik nama kolom di baris 12. Menulis
f["Volumee"]menghasilkanKeyError: 'Volumee'. Salin nama kolom dari tabel atribut. - Salah ketik nama kolom di baris 16. Menulis
"Volumee" IS NOT NULLtidak menghasilkan galat di baris itu. Saringan tidak bekerja, dan galat muncul kemudian di baris 33:TypeError: '<' not supported between instances of 'float' and 'NoneType'. PetakG08ikut terhitung, dan angka kosongnya tidak bisa dibandingkan. Pelajarannya: galat bisa muncul jauh dari sumber salahnya. - Petak kosong tidak digambar. Saya merender peta tanpa layer
Belum diukur. Hasilnya, kotak petakG08kosong (putih), karena penyaji bertingkat tidak punya kelas untuk nilai kosong. Itulah alasan skrip menambah layer abu-abu tersendiri di baris 43-52. Jangan hapus layer itu bila petak kosong harus tetap terlihat. - Dijalankan ulang. Setiap kali skrip ini dijalankan, satu layer
Belum diukurbaru ditambahkan lagi (uji: dua kali jalan memberi dua layer bernamaBelum diukur). Hapus layer lama lebih dulu, atau mulai dari proyek baru. - Menimpa warna Skrip 17.1. Setelah skrip ini jalan, petak berwarna menurut volume, bukan menurut jenis. Itu wajar. Untuk kembali, jalankan ulang bagian penyaji di Skrip 17.1.
- Jangan membagi satu objek ke banyak penyaji. Catatan Buku B4 menyebut bahwa memakai ulang satu objek skala warna pada beberapa penyaji membuat QGIS berhenti mendadak. Karena itu baris 19 dan 24 membuat objek baru tiap kali. Hal ini tidak saya ulangi di sini. [CEK]
Latihan variasi.
- Pasang metode Natural Breaks dengan lima kelas pada layer, bukan Quantile empat kelas.
- Cetak batas kelas hasilnya.
Contoh jawaban. Ganti baris 37, lalu tambahkan satu baris print:
rend = pasang(QgsClassificationJenks, 5)
print("Pilihan latihan:", [(round(r.lowerValue(), 1), round(r.upperValue(), 1)) for r in rend.ranges()])Layar menampilkan Pilihan latihan: [(45.5, 60.4), (60.4, 89.1), (89.1, 161.4), (161.4, 226.4), (226.4, 254.1)].
Skrip 17.3: Warna untuk peta tinggi dan bayangan bukit #
Berkas skrip_4_2b_raster.py. Di dalam berkasnya tertulis "Skrip 4.2b". Skrip ini sedikit keluar dari alur, karena menangani raster, bukan vektor.
Tujuan. Memberi skala warna Viridis lima kelas pada peta tinggi (DTM), lalu membuat layer salinan yang menampilkan bayangan bukit.
Bahan. Berkas DTM_Drone.tif di folder kerja. Skrip ini berdiri sendiri, karena memuat raster sendiri.
Raster adalah gambar dari kotak-kotak kecil (piksel), dan tiap kotak menyimpan satu angka. Di sini angkanya tinggi tempat dalam meter, dari 15 sampai 25 meter. Raster dibahas lebih lengkap di Bab 16.
Tiga istilah baru:
- Pseudocolor: tiap rentang angka diberi satu warna.
- Hillshade: bayangan bukit, yang memberi kesan relief dari arah cahaya.
- Band: satu lapis angka dalam raster. DTM hanya punya satu.

Skrip utuh:
# Skrip 4.2b: simbologi raster. Peta tinggi (DTM) diberi skala warna, lalu dibuat versi bayangan bukit
from qgis.core import (QgsProject, QgsRasterLayer, QgsStyle, QgsColorRampShader,
QgsRasterShader, QgsSingleBandPseudoColorRenderer, QgsHillshadeRenderer, Qgis)
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
dtm = QgsRasterLayer(FOLDER + "/DTM_Drone.tif", "DTM_Drone")
assert dtm.isValid(), "Raster tidak terbaca: " + FOLDER + "/DTM_Drone.tif"
statistik = dtm.dataProvider().bandStatistics(1)
print("Tinggi terendah %.1f m, tertinggi %.1f m" % (statistik.minimumValue, statistik.maximumValue))
# 1. Pseudocolor: tiap rentang tinggi diberi warna dari skala Viridis
fungsi = QgsColorRampShader(statistik.minimumValue, statistik.maximumValue)
fungsi.setSourceColorRamp(QgsStyle.defaultStyle().colorRamp("Viridis"))
fungsi.setClassificationMode(Qgis.ShaderClassificationMethod.EqualInterval)
fungsi.classifyColorRamp(5) # 5 kelas warna sama lebar
bayangan_warna = QgsRasterShader()
bayangan_warna.setRasterShaderFunction(fungsi)
dtm.setRenderer(QgsSingleBandPseudoColorRenderer(dtm.dataProvider(), 1, bayangan_warna))
print("Kelas warna:", [(round(i.value, 1), i.color.name()) for i in fungsi.colorRampItemList()])
# 2. Hillshade (bayangan bukit) pada salinan layer: arah cahaya 315 derajat, tinggi matahari 45 derajat
bukit = QgsRasterLayer(FOLDER + "/DTM_Drone.tif", "DTM_Bayangan")
bukit.setRenderer(QgsHillshadeRenderer(bukit.dataProvider(), 1, 315, 45))
print("Renderer bukit:", bukit.renderer().type())
QgsProject.instance().addMapLayer(dtm)
QgsProject.instance().addMapLayer(bukit)Penjelasan baris demi baris.
| Baris | Tulisan | Artinya |
|---|---|---|
| 2-3 | from qgis.core import (...) | Panggil delapan perkakas, termasuk Qgis yang memuat daftar pilihan bernama. |
| 5 | FOLDER = ... | Lokasi folder kerja. Ganti dengan jalur Anda. |
| 7 | dtm = QgsRasterLayer(FOLDER + "/DTM_Drone.tif", "DTM_Drone") | Buka raster sebagai layer. Hanya dua isian: alamat dan nama. Raster tidak butuh isian "ogr". |
| 8 | assert dtm.isValid(), "Raster tidak terbaca: " + ... | Pemeriksa (Bab 10). Bila raster tidak terbaca, skrip berhenti dengan AssertionError dan pesan Raster tidak terbaca: diikuti jalur berkasnya. |
| 9 | statistik = dtm.dataProvider().bandStatistics(1) | Minta penyedia data layer menghitung statistik band nomor 1: terendah, tertinggi, rata-rata. |
| 10 | print("Tinggi terendah %.1f m, ..." % (...)) | Tampilkan tinggi terendah dan tertinggi, satu desimal. |
| 13 | fungsi = QgsColorRampShader(min, max) | Buat pengatur warna untuk rentang dari tinggi terendah sampai tertinggi. |
| 14 | fungsi.setSourceColorRamp(...colorRamp("Viridis")) | Pakai skala warna Viridis. Skala ini bergerak dari ungu tua ke kuning. |
| 15 | fungsi.setClassificationMode(...EqualInterval) | Pembagian kelas dengan selebar sama. Pilihan namanya diambil dari perkakas Qgis. |
| 16 | fungsi.classifyColorRamp(5) | Bagi skala warna menjadi lima kelas. |
| 17-18 | bayangan_warna = QgsRasterShader() lalu setRasterShaderFunction(fungsi) | Bungkus pengatur warna ke dalam "pewarna raster". Nama kotak bayangan_warna membingungkan, karena isinya pewarna biasa, bukan bayangan bukit. |
| 19 | dtm.setRenderer(QgsSingleBandPseudoColorRenderer(...)) | Pasang penyaji pseudocolor pada layer: band 1, diwarnai oleh pewarna tadi. |
| 20 | print("Kelas warna:", [(round(i.value, 1), i.color.name()) ...]) | Tampilkan tiap kelas: nilai batasnya dan kode warnanya. |
| 23 | bukit = QgsRasterLayer(...) | Buka berkas yang sama sekali lagi sebagai layer baru, DTM_Bayangan. |
| 24 | bukit.setRenderer(QgsHillshadeRenderer(provider, 1, 315, 45)) | Pasang penyaji bayangan bukit. Angka 315 adalah arah cahaya dalam derajat (dari barat laut). Angka 45 adalah tinggi matahari dalam derajat. |
| 25 | print("Renderer bukit:", bukit.renderer().type()) | Tampilkan jenis penyaji, untuk memastikan terpasang. |
| 27-28 | QgsProject.instance().addMapLayer(...) | Tambahkan kedua layer ke proyek. bukit ditambah belakangan, jadi berada di atas dtm. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Tinggi terendah 15.0 m, tertinggi 25.0 m
Kelas warna: [(15.0, '#440154'), (17.5, '#3b528b'), (20.0, '#21908d'), (22.5, '#5dc963'), (25.0, '#fde725')]
Renderer bukit: hillshadeLima nilai batas tampil: 15,0, 17,5, 20,0, 22,5, dan 25,0 meter. Warna pertama #440154 adalah ungu tua, dan warna terakhir #fde725 adalah kuning. Data DTM latihan memang buatan, jadi rentang tingginya sempit.
Bagian yang boleh diubah.
- Baris 7 dan 23, nama berkas. Ganti
DTM_Drone.tifdengan raster lain. Ganti di kedua baris. - Baris 14, nama skala warna. Contoh:
"Magma". - Baris 16, jumlah kelas. Angka 5 boleh diganti.
- Baris 24, 315 dan 45. Ubah arah dan tinggi cahaya. Contoh
225, 30memberi bayangan lebih panjang dari arah barat daya.
Jebakan.
- Berkas tidak ditemukan menghentikan skrip. Bila jalur salah, baris 8 menghentikan skrip dengan
AssertionError: Raster tidak terbaca:diikuti jalur lengkapnya. Tanpa baris itu, skrip tetap berjalan dan mencetak tinggi terendah sekitar 1,8 kali 10 pangkat 308, tinggi tertinggi minus angka yang sama, laluKelas warna: []. Angka raksasa itu tanda raster tidak terbaca. Bila pesan muncul, periksa jalur di baris 5. - Nama skala warna salah. Menulis
"Viridiss"tidak menimbulkan galat. QGIS diam-diam memakai skala lain, dari biru ke merah. Uji: warna pertama#0000ff, warna terakhir#cc0033. PeriksaKelas warnapada hasil. - Bayangan bukit menutup peta. Layer
DTM_Bayanganberada di atas semua layer lain. Bila skrip ini dijalankan di proyek yang sama sebelum Skrip 17.5, layout hasil ekspor tertutup bidang abu-abu, dan petak tidak tampak. Hal ini saya lihat langsung dari hasil ekspor. Jalankan skrip ini di proyek terpisah, atau setelah layout diekspor. - Dua layer dari satu berkas. Ini sengaja. Satu layer tidak bisa memakai dua penyaji sekaligus, jadi skrip membuat salinan.
Latihan variasi. Ubah skrip menjadi tiga kelas, skala warna Magma, dan cahaya dari arah 225 derajat dengan tinggi matahari 30 derajat.
Contoh jawaban, hanya bagian yang berubah:
fungsi.setSourceColorRamp(QgsStyle.defaultStyle().colorRamp("Magma"))
fungsi.classifyColorRamp(3)
bukit.setRenderer(QgsHillshadeRenderer(bukit.dataProvider(), 1, 225, 30))Layar menampilkan Kelas warna: [(15.0, '#000004'), (20.0, '#b6377a'), (25.0, '#fcfdbf')] dan Renderer bukit: hillshade.
Skrip 17.4: Label berisi kode petak dan volume #
Berkas skrip_4_3_label.py. Di dalam berkasnya tertulis "Skrip 4.3".
Tujuan. Memberi label pada tiap petak berisi kode dan volumenya, dengan garis tepi putih agar terbaca, lalu menguji hasil ekspresi pada dua petak.
Bahan. Jalankan Skrip 17.1 lebih dulu, agar layer Petak_KPH sudah ada di proyek. Skrip ini tidak membaca berkas lain.
Label adalah tulisan di atas peta. Isinya bisa kolom biasa, bisa juga ekspresi (rumus kecil yang menyusun teks dari beberapa kolom). Garis tepi putih di sekeliling huruf disebut halo. Halo membuat huruf tetap terbaca di atas warna gelap.

Skrip utuh:
# Skrip 4.3: label petak berisi ID dan volume, dengan garis tepi putih agar terbaca
from qgis.core import (QgsProject, QgsPalLayerSettings, QgsTextFormat, QgsTextBufferSettings,
QgsVectorLayerSimpleLabeling, QgsExpression, QgsExpressionContext,
QgsExpressionContextUtils)
from qgis.PyQt.QtGui import QColor, QFont
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
petak = QgsProject.instance().mapLayersByName("Petak_KPH")[0]
ekspresi = ('"ID_Petak" || \'\n\' || '
'coalesce(to_string(round("Volume_m3ha")) || \' m3/ha\', \'belum diukur\')')
pengaturan = QgsPalLayerSettings()
pengaturan.fieldName = ekspresi
pengaturan.isExpression = True
teks = QgsTextFormat()
teks.setFont(QFont("Arial"))
teks.setSize(8)
teks.setColor(QColor("#202020"))
garis = QgsTextBufferSettings()
garis.setEnabled(True)
garis.setSize(0.8)
garis.setColor(QColor("white"))
teks.setBuffer(garis)
pengaturan.setFormat(teks)
petak.setLabeling(QgsVectorLayerSimpleLabeling(pengaturan))
petak.setLabelsEnabled(True)
petak.triggerRepaint()
# periksa hasil ekspresi pada dua petak
konteks = QgsExpressionContext()
konteks.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(petak))
for f in petak.getFeatures():
if f["ID_Petak"] in ("A01", "G08"):
konteks.setFeature(f)
print(f["ID_Petak"], "->", repr(QgsExpression(ekspresi).evaluate(konteks)))Penjelasan baris demi baris. Garis tegak ganda || di dalam ekspresi berfungsi menyambung teks. Karena tanda garis tegak mengganggu tabel, sel di bawah menyebutnya "garis tegak ganda".
| Baris | Tulisan | Artinya |
|---|---|---|
| 2-5 | from qgis.core import (...) dan from qgis.PyQt.QtGui import QColor, QFont | Panggil perkakas label dari QGIS, lalu perkakas warna dan huruf dari PyQt (perangkat jendela yang dipakai QGIS). |
| 7 | FOLDER = ... | Didefinisikan, tetapi tidak dipakai di skrip ini. Tidak berbahaya. |
| 9 | petak = ...mapLayersByName("Petak_KPH")[0] | Cari layer Petak_KPH di proyek. |
| 11-12 | ekspresi = (... '...' ...) | Rumus label, ditulis sebagai teks dalam dua potongan yang disambung otomatis oleh tanda kurung. Hasilnya, dalam bahasa QGIS: kode petak, lalu baris baru, lalu volume yang dibulatkan diikuti m3/ha. Bila volumenya kosong, tampil belum diukur. Penjelasan bagian-bagiannya ada di bawah tabel. |
| 14 | pengaturan = QgsPalLayerSettings() | Buat kotak pengaturan label yang masih kosong. |
| 15-16 | pengaturan.fieldName = ekspresi, pengaturan.isExpression = True | Isi pengaturan: teks yang dilabel adalah ekspresi, dan teks itu harus dibaca sebagai rumus, bukan nama kolom. Titik di tengah artinya "milik pengaturan ini" (Bab 11). |
| 18 | teks = QgsTextFormat() | Buat kotak format teks yang kosong. |
| 19-21 | teks.setFont(QFont("Arial")), setSize(8), setColor(QColor("#202020")) | Atur jenis huruf Arial, ukuran 8, dan warna abu sangat tua. |
| 22-26 | garis = QgsTextBufferSettings() dan baris di bawahnya | Buat pengaturan halo. Nyalakan, atur tebal 0,8, warna putih, lalu pasang ke format teks. |
| 27 | pengaturan.setFormat(teks) | Pasang format teks ke pengaturan label. |
| 29 | petak.setLabeling(QgsVectorLayerSimpleLabeling(pengaturan)) | Pasang pengaturan sebagai label layer. "Simple" berarti satu pengaturan untuk semua fitur. |
| 30-31 | petak.setLabelsEnabled(True), petak.triggerRepaint() | Nyalakan label, lalu gambar ulang. |
| 34-35 | konteks = QgsExpressionContext() dan appendScopes(...) | Siapkan "ruang pandang" untuk menghitung ekspresi: variabel global, proyek, dan layer ini. |
| 36-37 | for f in petak.getFeatures(): if f["ID_Petak"] in ("A01", "G08"): | Ulangi untuk tiap petak, tetapi hanya proses petak A01 dan G08. |
| 38 | konteks.setFeature(f) | Beritahu ruang pandang: petak ini yang sedang dihitung. |
| 39 | print(..., repr(QgsExpression(ekspresi).evaluate(konteks))) | Hitung ekspresi pada petak itu, lalu cetak dengan repr. Fungsi repr menampilkan tanda khusus apa adanya, jadi baris baru tampil sebagai \n. |
Membaca ekspresi di baris 11-12. Rumus label itu terdiri dari empat bagian. Garis tegak ganda menyambung dua teks. round membulatkan angka ke bilangan bulat. to_string mengubah angka menjadi teks, supaya bisa disambung.
coalesce mengambil isian pertama yang tidak kosong, dan bila semuanya kosong dipakai isian terakhir. Karena angka kosong disambung dengan teks hasilnya kosong, petak G08 jatuh ke teks belum diukur.
Dua tanda di baris 11-12 sering membingungkan. Pertama, \' adalah kutip tunggal yang ditulis di dalam teks berkutip tunggal. Garis miring terbalik di depannya berarti "ini bagian teks, bukan penutup".
Kedua, \n adalah baris baru. Karena itu label tampil dalam dua baris.
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
A01 -> 'A01\n46 m3/ha'
G08 -> 'G08\nbelum diukur'Petak A01 bervolume 45,5, dan hasilnya dibulatkan menjadi 46. Di peta, tiap petak yang digambar punya label dua baris. Label G08 berbunyi "belum diukur" bila petak itu digambar (lihat Jebakan).
Bagian yang boleh diubah.
- Baris 11-12, ekspresi. Ganti untuk menyusun label lain. Nama kolom ditulis dengan kutip ganda, dan teks biasa dengan kutip tunggal.
- Baris 19, jenis huruf. Ganti
"Arial"dengan huruf yang terpasang di komputer Anda. - Baris 20, ukuran. Ganti angka 8.
- Baris 21, warna. Ganti kode warna huruf.
- Baris 24, tebal halo. Ganti 0,8. Nilai 0 mematikan efek.
- Baris 37, ID petak. Ganti
"A01", "G08"dengan petak yang ingin Anda periksa.
Jebakan.
- Lupa garis miring terbalik. Menulis ekspresi baru dengan kutip tunggal di dalam kutip tunggal, misalnya
'"ID_Petak" || ' - ' || "Jenis"', menghasilkanTypeError: unsupported operand type(s) for -: 'str' and 'str'. Python mengira tanda kutip di tengah menutup teks. Perbaikan: tulis\' - \'. - Skrip 17.1 belum dijalankan. Layar menampilkan
IndexError: list index out of rangepada baris 9. - Nama layer diganti di panel Layers. Baris 9 mencari layer bernama
Petak_KPH. Bila Anda mengganti nama layer itu, pencarian menghasilkan daftar kosong dan layar menampilkanIndexErroryang sama. Samakan nama di baris 9 dengan nama layer yang sekarang. Skrip 17.5 tidak mengubah nama layer, jadi skrip ini tetap bisa dijalankan sesudahnya. - Label
G08hilang setelah Skrip 17.2. Hasil render saya: bila skrip ini dijalankan setelah Skrip 17.1 saja, petakG08berlabelbelum diukur. Tetapi bila Skrip 17.2 sudah jalan, penyaji bertingkat tidak punya kelas untuk nilai kosong, sehinggaG08tidak digambar dan labelnya ikut hilang. Hanya 15 petak yang berlabel. Ekspresicoalescetetap benar, seperti terlihat pada hasil uji skrip (baris 39). Yang berbeda adalah petak itu tidak dirender. Menurut saya, label hanya dibuat untuk fitur yang digambar. [kemungkinan] FOLDERtidak dipakai. Jalur yang salah tidak berpengaruh di skrip ini.
Latihan variasi. Ubah label menjadi kode petak diikuti jenisnya, dalam satu baris, dipisah - . Contoh: A01 - Jati.
Contoh jawaban. Ganti baris 11-12 dengan satu baris:
ekspresi = '"ID_Petak" || \' - \' || "Jenis"'Layar menampilkan A01 -> 'A01 - Jati' dan G08 -> 'G08 - Agroforestri'.
Skrip 17.5: Menyusun lembar peta A4 #
Berkas skrip_4_4_layout.py. Di dalam berkasnya tertulis "Skrip 4.4". Skrip ini yang terpanjang kedua di bab ini, tetapi isinya berulang. Ia memasang enam item satu per satu, dengan pola yang hampir sama.
Tujuan. Menyusun layout peta A4 lanskap yang lengkap (peta, judul, legenda, batang skala, panah utara, catatan), lalu mengekspornya ke PDF dan PNG.
Bahan. Layer hasil Skrip 17.1, 17.2, dan 17.4 sudah ada di proyek: Petak_KPH, Hotspot_Nasional, Sungai, Jalan, dan Belum diukur (dari Skrip 17.2). Skrip ini mengekspor dua berkas ke FOLDER: Peta_KPH.pdf dan Peta_KPH.png.
Lembar peta dibangun dari item: kotak-kotak yang ditaruh di halaman, masing-masing berisi satu hal. Letak dan ukuran item diukur dalam milimeter. Titik acuannya pojok kiri atas halaman. Bayangkan menempel stiker di kertas A4 dengan penggaris.

Pada skala 1:3000, satu milimeter di kertas sama dengan 3000 milimeter, atau 3 meter, di lapangan. Maka peta 190 kali 140 milimeter menampilkan area 570 kali 420 meter. Data latihan berukuran 400 kali 400 meter, jadi pas.

Skrip utuh:
# Skrip 4.4: menyusun layout peta A4 lanskap (peta, judul, legenda, skala, panah utara) dan mengekspornya
from qgis.core import (QgsProject, QgsPrintLayout, QgsLayoutItemMap, QgsLayoutItemLabel, QgsLayoutItemLegend,
QgsLayoutItemScaleBar, QgsLayoutItemPicture, QgsLayoutItemShape, QgsLayoutPoint, QgsLayoutSize,
QgsLayoutExporter, QgsRectangle, QgsApplication, QgsLayerTree, QgsLegendStyle,
QgsTextFormat, Qgis, QgsMapLayerLegendUtils)
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QColor, QFont
import os
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
proyek = QgsProject.instance()
mm = Qgis.LayoutUnit.Millimeters
lama = proyek.layoutManager().layoutByName("Peta_KPH_A4")
if lama is not None: # dijalankan ulang: buang layout lama agar nama tidak bentrok
proyek.layoutManager().removeLayout(lama)
tata = QgsPrintLayout(proyek)
tata.initializeDefaults() # halaman A4 lanskap (297 x 210 mm)
tata.setName("Peta_KPH_A4")
proyek.layoutManager().addLayout(tata)
# 1. Peta: ukuran 190 x 140 mm pada skala 1:3000
peta = QgsLayoutItemMap(tata)
peta.setCrs(proyek.crs())
peta.attemptMove(QgsLayoutPoint(10, 28, mm))
peta.attemptResize(QgsLayoutSize(190, 140, mm)) # ukuran dan letak diatur lebih dulu
peta.zoomToExtent(QgsRectangle(312000, 9996000, 312400, 9996400)) # pas ke area KPH
peta.setScale(3000) # skala diatur terakhir
peta.setFrameEnabled(True)
tata.addLayoutItem(peta)
# 2. Judul
judul = QgsLayoutItemLabel(tata)
judul.setText("PETA KPH CONTOH: VOLUME KAYU PER PETAK")
fmt = QgsTextFormat()
fmt.setFont(QFont("Arial", 18, QFont.Weight.Bold))
fmt.setSize(18)
judul.setTextFormat(fmt)
judul.attemptMove(QgsLayoutPoint(10, 8, mm))
judul.attemptResize(QgsLayoutSize(277, 14, mm))
tata.addLayoutItem(judul)
# 3. Legenda: dibuat manual agar nama entrinya ramah dibaca
legenda = QgsLayoutItemLegend(tata)
legenda.setLinkedMap(peta)
legenda.setSyncMode(Qgis.LegendSyncMode.Manual) # isi legenda tidak ikut berubah otomatis
akar = legenda.model().rootGroup()
nama_baru = {"Petak_KPH": "Volume kayu (m3/ha)", "Hotspot_Nasional": "Titik panas",
"Sungai": "Sungai", "Jalan": "Jalan", "Belum diukur": "Petak belum diukur"}
for simpul in list(akar.children()):
if simpul.name() in nama_baru:
baru = nama_baru[simpul.name()]
if len(legenda.model().layerLegendNodes(simpul)) == 1: # entri tunggal: ganti label entri (nama layer di proyek tetap)
QgsMapLayerLegendUtils.setLegendNodeUserLabel(simpul, 0, baru)
legenda.model().refreshLayerLegend(simpul)
else: # beberapa entri: ganti judul kelompoknya
simpul.setCustomProperty("legend/title-label", baru)
legenda.setTitle("Legenda")
legenda.attemptMove(QgsLayoutPoint(205, 28, mm))
tata.addLayoutItem(legenda)
# 4. Batang skala: 2 segmen, tiap segmen 100 m
skala = QgsLayoutItemScaleBar(tata)
skala.setLinkedMap(peta)
skala.setStyle("Single Box")
skala.setUnits(Qgis.DistanceUnit.Meters)
skala.setUnitLabel("m")
skala.setNumberOfSegments(2)
skala.setNumberOfSegmentsLeft(0)
skala.setUnitsPerSegment(100)
skala.attemptMove(QgsLayoutPoint(12, 174, mm))
tata.addLayoutItem(skala)
# 5. Panah utara (gambar SVG bawaan QGIS)
panah = QgsLayoutItemPicture(tata)
for folder in QgsApplication.svgPaths():
berkas = os.path.join(folder, "arrows", "NorthArrow_02.svg")
if os.path.exists(berkas):
panah.setPicturePath(berkas)
break
panah.attemptMove(QgsLayoutPoint(270, 28, mm))
panah.attemptResize(QgsLayoutSize(15, 15, mm))
panah.setLinkedMap(peta) # panah ikut arah utara peta
tata.addLayoutItem(panah)
# 6. Teks skala angka, CRS, sumber, dan penulis
catatan = QgsLayoutItemLabel(tata)
catatan.setText("Skala 1:3.000 (A4). Sistem koordinat: WGS 84 / UTM zona 49S (EPSG:32749).\n"
"Sumber: data sintetis latihan, bukan kondisi lapangan. Penyusun: Analis Muda, KPH Contoh.")
fmt2 = QgsTextFormat()
fmt2.setFont(QFont("Arial"))
fmt2.setSize(8)
catatan.setTextFormat(fmt2)
catatan.attemptMove(QgsLayoutPoint(10, 192, mm))
catatan.attemptResize(QgsLayoutSize(277, 12, mm))
tata.addLayoutItem(catatan)
# 7. Ekspor
pengekspor = QgsLayoutExporter(tata)
hasil_pdf = pengekspor.exportToPdf(os.path.join(FOLDER, "Peta_KPH.pdf"), QgsLayoutExporter.PdfExportSettings())
pengaturan = QgsLayoutExporter.ImageExportSettings()
pengaturan.dpi = 150
hasil_png = pengekspor.exportToImage(os.path.join(FOLDER, "Peta_KPH.png"), pengaturan)
print("PDF:", hasil_pdf, " PNG:", hasil_png)
print("Skala peta: 1:%d" % round(peta.scale()), " Tinggi peta (mm):", round(peta.rect().height()))Penjelasan baris demi baris. Enam item memakai pola yang sama: buat item, atur letak dan ukuran, isi, lalu addLayoutItem. Karena itu tabel ini dikelompokkan per item.
| Baris | Tulisan | Artinya |
|---|---|---|
| 2-8 | from ... import (...), import os | Panggil banyak perkakas layout. Lima di antaranya tidak dipakai di skrip ini: QgsLayoutItemShape, QgsLayerTree, QgsLegendStyle, Qt, dan QColor. Impor yang tersisa tidak berpengaruh. Perkakas QgsMapLayerLegendUtils dipakai untuk mengganti tulisan entri legenda. |
| 10-12 | FOLDER, proyek, mm | Folder kerja, proyek yang terbuka, dan satuan milimeter yang disimpan di kotak mm agar tidak ditulis panjang. |
| 14-16 | lama = ...layoutByName("Peta_KPH_A4"), if lama is not None:, removeLayout(lama) | Cari layout bernama Peta_KPH_A4 di proyek. Bila ada (is not None), buang dulu. Dengan begitu skrip aman dijalankan ulang, karena dua layout tidak boleh sama namanya. |
| 17 | tata = QgsPrintLayout(proyek) | Buat layout baru di proyek ini. |
| 18 | tata.initializeDefaults() | Isi pengaturan awal: satu halaman A4 lanskap, 297 kali 210 mm. |
| 19-20 | tata.setName("Peta_KPH_A4"), ...addLayout(tata) | Beri nama layout, lalu daftarkan ke manajer layout proyek. Nama ini dipakai Skrip 17.6. |
| 23-24 | peta = QgsLayoutItemMap(tata), peta.setCrs(proyek.crs()) | Buat item peta. Pakai sistem koordinat yang sama dengan proyek. |
| 25-26 | attemptMove(QgsLayoutPoint(10, 28, mm)), attemptResize(QgsLayoutSize(190, 140, mm)) | Letakkan pojok kiri atas peta pada 10 mm dari kiri dan 28 mm dari atas. Ukurannya 190 kali 140 mm. Kata attempt berarti "coba": QGIS bisa menolak bila ada penyebab lain. |
| 27 | peta.zoomToExtent(QgsRectangle(...)) | Arahkan isi peta ke satu persegi panjang: kiri, bawah, kanan, atas, dalam koordinat UTM. |
| 28 | peta.setScale(3000) | Atur skala menjadi 1:3000. Komentar mengingatkan: skala diatur terakhir. |
| 29-30 | setFrameEnabled(True), tata.addLayoutItem(peta) | Beri bingkai di sekeliling peta, lalu tambahkan item ke halaman. |
| 33-38 | judul = QgsLayoutItemLabel(tata) ... setTextFormat(fmt) | Buat item label. Isi tulisannya, atur huruf Arial 18 tebal, lalu pasang format. QFont.Weight.Bold berarti tebal. |
| 39-41 | attemptMove, attemptResize, addLayoutItem | Letakkan judul di pojok 10 mm dari kiri dan 8 mm dari atas, selebar 277 mm. Lalu tambahkan. |
| 44-46 | legenda = QgsLayoutItemLegend(tata), setLinkedMap, setSyncMode(...Manual) | Buat legenda, hubungkan ke item peta, lalu pilih mode manual. Mode manual berarti isi legenda tidak ikut berubah otomatis, jadi nama entrinya boleh diganti. |
| 47 | akar = legenda.model().rootGroup() | Ambil isi legenda, yaitu daftar entri layer. |
| 48-49 | nama_baru = {...} | Kamus: nama layer lama dan nama ramah yang diinginkan, misalnya Petak_KPH menjadi Volume kayu (m3/ha). |
| 50-57 | for simpul in list(akar.children()): ... | Ulangi untuk tiap entri legenda. Bila nama layer ada di kamus, ambil nama barunya (baris 52). Layer yang hanya punya satu entri (baris 53) diganti tulisan entrinya dengan setLegendNodeUserLabel, dan nama layer di proyek tetap. Layer berentri banyak, yaitu Petak_KPH, diganti judul kelompoknya lewat properti legend/title-label (baris 56-57). Fungsi list(...) membuat salinan daftar agar aman dibaca sambil diubah. |
| 58-60 | setTitle, attemptMove, addLayoutItem | Beri judul legenda, letakkan pada 205 mm dari kiri dan 28 mm dari atas, lalu tambahkan. |
| 63-70 | skala = QgsLayoutItemScaleBar(tata) ... setUnitsPerSegment(100) | Buat batang skala: gaya Single Box, satuan meter, 2 segmen, tiap segmen 100 meter, dan tanpa segmen di kiri nol. Total panjang 200 meter. |
| 71-72 | attemptMove, addLayoutItem | Letakkan di bawah peta, lalu tambahkan. |
| 75 | panah = QgsLayoutItemPicture(tata) | Buat item gambar untuk panah utara. |
| 76-80 | for folder in QgsApplication.svgPaths(): ... break | Cari berkas NorthArrow_02.svg di semua folder gambar bawaan QGIS. Begitu ditemukan, pasang sebagai gambar, lalu break menghentikan pencarian. |
| 81-84 | attemptMove, attemptResize, setLinkedMap(peta), addLayoutItem | Letakkan panah di kanan atas, ukuran 15 kali 15 mm. Hubungkan ke peta supaya panah ikut arah utara peta. |
| 87-93 | catatan = QgsLayoutItemLabel(tata) ... setTextFormat(fmt2) | Buat label kedua berisi skala, sistem koordinat, sumber, dan penyusun. Dua potongan teks disambung otomatis oleh tanda kurung, dan \n memecah baris. Huruf Arial ukuran 8. |
| 94-96 | attemptMove, attemptResize, addLayoutItem | Letakkan di dasar halaman, selebar 277 mm. |
| 99 | pengekspor = QgsLayoutExporter(tata) | Buat pengekspor untuk layout ini. |
| 100 | hasil_pdf = pengekspor.exportToPdf(...) | Ekspor ke PDF. Hasilnya angka: 0 berarti berhasil. |
| 101-103 | pengaturan = ...ImageExportSettings(), pengaturan.dpi = 150, exportToImage(...) | Ekspor ke PNG pada 150 dpi. Dibahas lebih lanjut di Skrip 17.6. |
| 104-105 | print(...) | Cetak hasil ekspor, lalu skala dan tinggi peta. round(peta.scale()) membulatkan skala; %d mencetaknya sebagai bilangan bulat. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2, urutan 17.1, 17.2, 17.4, 17.5):
ERROR 6: The PNG driver does not support update access to existing datasets.
PDF: 0 PNG: 0
Skala peta: 1:3000 Tinggi peta (mm): 140Baris ERROR 6 berasal dari pustaka gambar GDAL dan tidak berarti ekspor gagal. Buktinya ada di baris di bawahnya: PDF: 0 PNG: 0 berarti kedua ekspor berhasil, dan kedua berkas ada di folder kerja. Pesan itu muncul pada setiap ekspor PNG di uji ini.
Lembar yang dihasilkan memuat judul "PETA KPH CONTOH: VOLUME KAYU PER PETAK", peta petak berwarna dengan label, legenda, batang skala 0 sampai 200 meter, panah utara, dan catatan sumber. Entri legenda tertulis Volume kayu (m3/ha) dan Petak belum diukur. Nama layer di proyek tidak berubah: tetap Belum diukur, Hotspot_Nasional, Jalan, Petak_KPH, dan Sungai.
Bagian yang boleh diubah.
- Baris 25-26 dan seterusnya, angka letak. Semua angka di
QgsLayoutPoint(x, y, mm)danQgsLayoutSize(lebar, tinggi, mm)dalam milimeter. Ubah untuk menggeser atau memperbesar item. - Baris 27, ekstensi. Ganti keempat koordinat dengan area yang ingin ditampilkan, dalam satuan peta.
- Baris 28, skala. Ganti 3000, misalnya 4000 untuk area lebih luas.
- Baris 34, judul. Ganti teks judul.
- Baris 48-49, nama entri legenda. Ganti isi kamus. Kunci kiri harus persis sama dengan nama layer.
- Baris 67-70, batang skala. Ganti jumlah segmen dan panjang tiap segmen.
- Baris 88-89, catatan. Ganti teks catatan.
- Baris 102,
dpi. Ganti 150 untuk resolusi PNG lain.
Jebakan.
- Legenda manual hanya mengganti tulisan, bukan nama layer. Baris 50-57 mengubah tulisan di legenda, sedangkan nama layer di panel Layers tetap
Petak_KPH,Hotspot_Nasional, dan seterusnya. Karena itu skrip lain yang mencari layer menurut nama tetap bisa jalan. Bila Anda mengganti baris 54 atau 57 dengansimpul.setName(...), nama layer di proyek ikut berubah. Pada uji, menjalankan Skrip 17.2 sesudah itu menghasilkanIndexError: list index out of range. Kunci kiri di kamus baris 48-49 harus memakai nama layer yang sebenarnya. - Dijalankan ulang. Layout bernama sama tidak boleh ada dua. Baris 14-16 mencari layout
Peta_KPH_A4yang lama dan membuangnya sebelum layout baru dibuat, jadi skrip aman diulang. Tanpa tiga baris itu, QGIS menolak layout kedua dan skrip gagal denganRuntimeError: wrapped C/C++ object of type QgsPrintLayout has been deleted. Ingat bahwa layout lama ikut terbuang. Bila Anda sudah mengubah layout itu dengan tangan, ubahan itu hilang. Untuk menyimpan keduanya, ganti nama di baris 19. - Urutan mengatur peta. Bila Anda menukar baris 27 dan 28 (skala diatur dulu, lalu
zoomToExtent), skala berubah mengikuti area. Uji saya: skala menjadi1:2857, bukan1:3000. Atur ukuran dulu, laluzoomToExtent, lalusetScale. - Angka tetap. Ekstensi di baris 27 hanya cocok dengan data latihan. Tulisan
Skala 1:3.000di baris 88 diketik manual. Bila Anda mengganti skala di baris 28, teks catatan tidak ikut berubah. Samakan dua-duanya. - Panah utara tidak ketemu. Bila nama berkas SVG di baris 77 salah, skrip tidak menimbulkan galat. Uji:
panah.picturePath()kosong (''), dan panah tidak tampil. Cek bahwa panah muncul di hasil. - Layer raster dari Skrip 17.3 ikut. Bila Skrip 17.3 sudah dijalankan di proyek yang sama, layer bayangan bukit menutup petak. Hasil uji: peta abu-abu. Legenda juga memuat entri
DTM_BayangandanDTM_Dronedengan nama aslinya. - Pesan
ERROR 6tidak berbahaya. Lihat bagian Hasil.
Latihan variasi. Ubah peta menjadi skala 1:4000, dan perbarui tulisan di catatan agar tetap benar.
Contoh jawaban. Ganti dua baris:
peta.setScale(4000) # skala diatur terakhircatatan.setText("Skala 1:4.000 (A4). Sistem koordinat: WGS 84 / UTM zona 49S (EPSG:32749).\n"Layar menampilkan Skala peta: 1:4000 Tinggi peta (mm): 140.
Skrip 17.6: Ekspor ke PDF dan PNG tiga resolusi #
Berkas skrip_4_5_ekspor.py. Di dalam berkasnya tertulis "Skrip 4.5". Skrip ini pendek, dan sebagian besar logikanya sudah Anda kenal dari baris 99-105 Skrip 17.5.
Tujuan. Mengekspor layout Peta_KPH_A4 ke satu PDF dan tiga PNG dengan resolusi berbeda, lalu mencatat ukuran tiap berkas.
Bahan. Layout Peta_KPH_A4 dari Skrip 17.5 sudah ada di proyek. Hasilnya empat berkas di FOLDER: Peta_KPH_A4.pdf, Peta_KPH_72.png, Peta_KPH_150.png, dan Peta_KPH_300.png.
dpi (dots per inch) adalah banyaknya titik gambar tiap inci kertas. Bayangkan sulaman: makin rapat jahitannya, makin halus gambarnya, tetapi makin besar bahan yang dipakai. PDF menyimpan huruf dan garis sebagai rumus (vektor), jadi tetap tajam di ukuran berapa pun. PNG menyimpan kotak-kotak piksel, jadi ketajamannya ditentukan dpi.


Skrip utuh:
# Skrip 4.5: mengekspor layout Peta_KPH_A4 ke PDF dan PNG pada beberapa resolusi, lalu mencatat ukuran berkas
import os
from qgis.core import QgsProject, QgsLayoutExporter
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
tata = QgsProject.instance().layoutManager().layoutByName("Peta_KPH_A4")
assert tata is not None, "Layout Peta_KPH_A4 belum ada; jalankan skrip 4.4 lebih dulu"
pengekspor = QgsLayoutExporter(tata)
# PDF: huruf dan garis tetap berupa vektor
pdf = os.path.join(FOLDER, "Peta_KPH_A4.pdf")
hasil = pengekspor.exportToPdf(pdf, QgsLayoutExporter.PdfExportSettings())
print("PDF hasil=%s ukuran=%d byte" % (hasil, os.path.getsize(pdf)))
# PNG: tiga resolusi
for dpi in (72, 150, 300):
pengaturan = QgsLayoutExporter.ImageExportSettings()
pengaturan.dpi = dpi
png = os.path.join(FOLDER, "Peta_KPH_%d.png" % dpi)
hasil = pengekspor.exportToImage(png, pengaturan)
from qgis.PyQt.QtGui import QImage
gambar = QImage(png)
print("PNG %3d dpi hasil=%s %d x %d piksel %d byte" % (dpi, hasil, gambar.width(), gambar.height(), os.path.getsize(png)))Penjelasan baris demi baris. Hanya hal yang baru dijelaskan.
| Baris | Tulisan | Artinya |
|---|---|---|
| 2-3 | import os, from qgis.core import ... | Panggil os untuk urusan jalur dan ukuran berkas, serta perkakas ekspor QGIS. |
| 7 | tata = QgsProject.instance().layoutManager().layoutByName("Peta_KPH_A4") | Ambil layout menurut namanya dari manajer layout. Bila nama tidak ada, hasilnya None. |
| 8 | assert tata is not None, "Layout ... belum ada; ..." | Pemeriksa (Bab 10). Bila layout tidak ditemukan, skrip berhenti dengan AssertionError dan pesan yang menyuruh Anda menjalankan Skrip 17.5 lebih dulu. Pesan itu menyebut "skrip 4.4", yaitu nomor di berkasnya. |
| 9 | pengekspor = QgsLayoutExporter(tata) | Buat pengekspor. |
| 12 | pdf = os.path.join(FOLDER, "Peta_KPH_A4.pdf") | Gabungkan folder dan nama berkas menjadi satu jalur, dengan pemisah yang benar untuk komputer Anda. |
| 13 | hasil = pengekspor.exportToPdf(pdf, ...PdfExportSettings()) | Ekspor ke PDF dengan pengaturan bawaan. Angka hasil disimpan di hasil. |
| 14 | print("PDF hasil=%s ukuran=%d byte" % (hasil, os.path.getsize(pdf))) | Cetak angka hasil dan ukuran berkas dalam byte. %s untuk teks atau angka apa pun, %d untuk bilangan bulat. |
| 17 | for dpi in (72, 150, 300): | Ulangi tiga kali. Di tiap putaran, dpi berisi satu nilai dari daftar. |
| 18-19 | pengaturan = ...ImageExportSettings(), pengaturan.dpi = dpi | Buat pengaturan gambar baru, lalu isi resolusinya. |
| 20 | png = os.path.join(FOLDER, "Peta_KPH_%d.png" % dpi) | Susun nama berkas yang memuat angka dpi, misalnya Peta_KPH_150.png. |
| 21 | hasil = pengekspor.exportToImage(png, pengaturan) | Ekspor ke PNG. |
| 22 | from qgis.PyQt.QtGui import QImage | Panggil perkakas pembaca gambar. Menaruhnya di dalam perulangan kurang lazim. Hasilnya tetap benar, karena Python hanya mengimpor sekali. Anda boleh memindahkannya ke atas. |
| 23 | gambar = QImage(png) | Buka PNG yang baru jadi, untuk membaca ukurannya. |
| 24 | print("PNG %3d dpi hasil=%s %d x %d piksel %d byte" % (...)) | Cetak dpi, angka hasil, lebar dan tinggi piksel, dan ukuran berkas. %3d memberi lebar tiga karakter agar sejajar. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2, urutan 17.1, 17.2, 17.4, 17.5, 17.6):
PDF hasil=0 ukuran=207837 byte
ERROR 6: The PNG driver does not support update access to existing datasets.
PNG 72 dpi hasil=0 841 x 595 piksel 68981 byte
ERROR 6: The PNG driver does not support update access to existing datasets.
PNG 150 dpi hasil=0 1753 x 1240 piksel 181906 byte
ERROR 6: The PNG driver does not support update access to existing datasets.
PNG 300 dpi hasil=0 3507 x 2480 piksel 404416 byteUkuran piksel selalu sama untuk dpi yang sama: 841 kali 595, 1753 kali 1240, dan 3507 kali 2480. Ukuran byte bergantung isi peta dan bisa sedikit berbeda di komputer Anda. Pesan ERROR 6 sama dengan di Skrip 17.5 dan tidak berbahaya.
Bagian yang boleh diubah.
- Baris 7, nama layout. Harus persis sama dengan nama di Skrip 17.5. Ubah juga nama di pesan baris 8.
- Baris 17, daftar dpi. Ganti isi tanda kurung, misalnya
(96, 200). - Baris 12 dan 20, nama berkas. Ganti nama sesuka Anda. Nama di baris 12 beda dengan
Peta_KPH.pdfdari Skrip 17.5, jadi hasilnya dua PDF.
Jebakan.
- Layout belum dibuat. Bila Skrip 17.5 belum jalan,
layoutByNamemengembalikanNone, dan baris 8 menghentikan skrip denganAssertionError: Layout Peta_KPH_A4 belum ada; .... Tanpa baris itu, skrip terus berjalan dan gagal belakangan denganFileNotFoundErrorpada nama berkas PDF. Pesan itu menyesatkan, karena masalah sebenarnya ada di layout, bukan di berkas. - Berkas lama tidak membuktikan ekspor berhasil. Folder bisa masih berisi PDF dan PNG dari proses sebelumnya, sehingga ukuran berkas yang tercetak bisa berasal dari berkas lama. Yang menentukan adalah angka
hasil. Nilai 0 berarti berhasil, nilai selain 0 berarti gagal. Selalu baca angka itu. - Berkas lama ditimpa. Berkas bernama sama diganti tanpa tanya. Pindahkan hasil penting sebelum menjalankan ulang.
- Resolusi sangat tinggi. Ukuran PNG naik cepat bersama dpi. Pada 300 dpi A4 sudah 3507 kali 2480 piksel. Tambah dpi hanya bila perlu.
Latihan variasi. Ekspor PNG hanya pada 96 dan 200 dpi, tanpa PDF.
Contoh jawaban. Hapus baris 11-14 dan ganti baris 17:
for dpi in (96, 200):Layar menampilkan PNG 96 dpi hasil=0 1122 x 793 piksel dan PNG 200 dpi hasil=0 2338 x 1653 piksel, masing-masing diikuti ukuran byte.
Skrip 17.7: Proyek mini dari data mentah sampai PDF #
Berkas skrip_4_6_jembatan.py. Di dalam berkasnya tertulis "Skrip 4.6". Skrip ini disebut "jembatan", karena menyambungkan kelas dasar dengan kelas menengah. Ia memadukan seleksi, alat Processing, simbologi, layout, dan ekspor dalam satu berkas.
Tujuan. Memeriksa sistem koordinat titik panas, lalu mengubahnya ke UTM. Setelah itu, menyeleksi petak jati bervolume besar dan membuat sempadan sungai 25 meter. Petak dan titik panas dipotong dengan sempadan itu. Terakhir, skrip membuat peta dan menyimpan proyek.
Bahan. Folder kerja berisi Hotspot_derajat.gpkg, Petak_KPH.gpkg, dan Sungai_Jalan.gpkg. Skrip berdiri sendiri, tetapi mengosongkan proyek. Hasilnya Peta_Sempadan.pdf dan KPH_Contoh_B4.qgz di FOLDER.
Lima istilah baru, yang dijelaskan lebih lengkap di Bab 18:
- Reproject: mengubah sistem koordinat sebuah layer.
- Buffer: zona selebar jarak tertentu di sekeliling objek.
- Clip: memotong sebuah layer dengan bentuk layer lain.
processing.run: perintah memanggil alat Processing dari skrip.assert: pemeriksa yang menghentikan skrip bila syaratnya tidak terpenuhi.
Sempadan sungai di skrip ini hanya angka latihan, 25 meter. Itu bukan ketentuan resmi.

Skrip utuh:
# Skrip 4.6 (Bab Jembatan): proyek mini terpadu B1-B4 dan pembuka geoprocessing.
# Alur: periksa CRS, reproject titik panas, seleksi atribut, buffer sungai, clip, simbologi, layout, ekspor.
import os
import processing
from qgis.core import (QgsProject, QgsVectorLayer, QgsCoordinateReferenceSystem, QgsFillSymbol,
QgsSingleSymbolRenderer, QgsMarkerSymbol, QgsLineSymbol, QgsPrintLayout,
QgsLayoutItemMap, QgsLayoutItemLabel, QgsLayoutItemLegend, QgsLayoutItemScaleBar,
QgsLayoutPoint, QgsLayoutSize, QgsLayoutExporter, QgsRectangle, QgsTextFormat, Qgis)
from qgis.PyQt.QtGui import QFont
FOLDER = r"D:/KPH_Contoh" # ganti dengan folder kerja Anda
JARAK_SEMPADAN = 25 # meter; angka latihan, bukan ketentuan resmi
proyek = QgsProject.instance()
proyek.clear()
proyek.setCrs(QgsCoordinateReferenceSystem("EPSG:32749"))
def muat(berkas, lapisan, nama):
layer = QgsVectorLayer("%s/%s|layername=%s" % (FOLDER, berkas, lapisan), nama, "ogr")
assert layer.isValid(), "Layer tidak terbaca: " + nama
return layer
# 1. Periksa CRS: titik panas kiriman kantor lain masih berderajat (B1)
mentah = muat("Hotspot_derajat.gpkg", "Hotspot_derajat", "Hotspot_mentah")
print("CRS titik panas mentah:", mentah.crs().authid(), "| satuan:", mentah.crs().mapUnits())
# 2. Reproject ke UTM 49S (alat Processing pertama)
hasil = processing.run("native:reprojectlayer", {
"INPUT": mentah, "TARGET_CRS": QgsCoordinateReferenceSystem("EPSG:32749"), "OUTPUT": "memory:Titik_Panas"})
titik = hasil["OUTPUT"]
print("Titik panas setelah reproject:", titik.featureCount(), "titik,", titik.crs().authid())
# 3. Data utama
petak = muat("Petak_KPH.gpkg", "Petak_KPH", "Petak KPH")
sungai = muat("Sungai_Jalan.gpkg", "Sungai", "Sungai")
# 4. Seleksi atribut (B2): petak jati bervolume tinggi
petak.selectByExpression('"Jenis" = \'Jati\' AND "Volume_m3ha" > 150')
print("Jati dengan volume > 150 m3/ha:", sorted(f["ID_Petak"] for f in petak.selectedFeatures()))
petak.removeSelection()
# 5. Buffer sungai (alat Processing kedua): sempadan contoh 25 m, digabung jadi satu bidang
sempadan = processing.run("native:buffer", {
"INPUT": sungai, "DISTANCE": JARAK_SEMPADAN, "SEGMENTS": 16, "DISSOLVE": True,
"OUTPUT": "memory:Sempadan"})["OUTPUT"]
luas_sempadan = sum(f.geometry().area() for f in sempadan.getFeatures())
print("Luas buffer sungai: %.0f m2 (%.2f ha)" % (luas_sempadan, luas_sempadan / 10000))
# 6. Clip petak dengan buffer: bagian petak yang masuk sempadan
petak_sempadan = processing.run("native:clip", {
"INPUT": petak, "OVERLAY": sempadan, "OUTPUT": "memory:Petak_di_sempadan"})["OUTPUT"]
total = 0
for f in sorted(petak_sempadan.getFeatures(), key=lambda x: x["ID_Petak"]):
luas = f.geometry().area()
total += luas
print(" %s %-6s %7.0f m2" % (f["ID_Petak"], f["Jenis"], luas))
print("Total petak dalam sempadan: %.0f m2 (%.2f ha), %d petak" % (total, total / 10000, petak_sempadan.featureCount()))
# 7. Clip titik panas dengan buffer: titik panas yang berada di dalam sempadan
titik_dalam = processing.run("native:clip", {
"INPUT": titik, "OVERLAY": sempadan, "OUTPUT": "memory:Titik_di_sempadan"})["OUTPUT"]
print("Titik panas di dalam sempadan:", titik_dalam.featureCount(), "dari", titik.featureCount())
# 8. Simbologi (B4) dan penyusunan layer; urutan: yang ditambah belakangan berada di atas
petak.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
{"color": "#E8F0D8", "outline_color": "#404040", "outline_width": "0.3"})))
sempadan.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
{"color": "#8FB8E0", "outline_color": "#2B6CB0", "outline_width": "0.2", "style": "solid"})))
petak_sempadan.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
{"color": "#F2A65A", "outline_color": "#8A4B00", "outline_width": "0.3"})))
sungai.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple({"line_color": "#2B6CB0", "line_width": "0.8"})))
titik.setRenderer(QgsSingleSymbolRenderer(QgsMarkerSymbol.createSimple(
{"name": "circle", "color": "#E03C31", "outline_color": "#FFFFFF", "size": "2.6"})))
for layer in (petak, sempadan, petak_sempadan, sungai, titik):
proyek.addMapLayer(layer)
petak_sempadan.setName("Petak di dalam sempadan")
sempadan.setName("Sempadan sungai %g m" % JARAK_SEMPADAN)
titik.setName("Titik panas")
# 9. Layout ringkas dan ekspor
mm = Qgis.LayoutUnit.Millimeters
tata = QgsPrintLayout(proyek)
tata.initializeDefaults()
tata.setName("Peta_Sempadan")
proyek.layoutManager().addLayout(tata)
peta = QgsLayoutItemMap(tata)
peta.setCrs(proyek.crs())
peta.attemptMove(QgsLayoutPoint(10, 28, mm))
peta.attemptResize(QgsLayoutSize(190, 140, mm))
peta.zoomToExtent(QgsRectangle(312000, 9996000, 312400, 9996400))
peta.setScale(3000)
peta.setFrameEnabled(True)
tata.addLayoutItem(peta)
judul = QgsLayoutItemLabel(tata)
judul.setText("SEMPADAN SUNGAI DI KPH CONTOH (CONTOH %g M)" % JARAK_SEMPADAN)
fmt = QgsTextFormat()
fmt.setFont(QFont("Arial", 16, QFont.Weight.Bold))
fmt.setSize(16)
judul.setTextFormat(fmt)
judul.attemptMove(QgsLayoutPoint(10, 10, mm))
judul.attemptResize(QgsLayoutSize(277, 12, mm))
tata.addLayoutItem(judul)
legenda = QgsLayoutItemLegend(tata)
legenda.setLinkedMap(peta)
legenda.setTitle("Legenda")
legenda.attemptMove(QgsLayoutPoint(205, 28, mm))
tata.addLayoutItem(legenda)
skala = QgsLayoutItemScaleBar(tata)
skala.setLinkedMap(peta)
skala.setStyle("Single Box")
skala.setUnits(Qgis.DistanceUnit.Meters)
skala.setUnitLabel("m")
skala.setNumberOfSegments(2)
skala.setNumberOfSegmentsLeft(0)
skala.setUnitsPerSegment(100)
skala.attemptMove(QgsLayoutPoint(12, 174, mm))
tata.addLayoutItem(skala)
hasil_pdf = QgsLayoutExporter(tata).exportToPdf(os.path.join(FOLDER, "Peta_Sempadan.pdf"), QgsLayoutExporter.PdfExportSettings())
print("Ekspor PDF:", hasil_pdf, "(0 = berhasil)")
proyek.write(os.path.join(FOLDER, "KPH_Contoh_B4.qgz"))
print("Proyek disimpan:", os.path.exists(os.path.join(FOLDER, "KPH_Contoh_B4.qgz")))Penjelasan baris demi baris. Bagian layout (baris 80-117) mengulang pola Skrip 17.5 dan dirangkum saja.
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-9 | komentar dan import | Dua komentar pembuka, lalu perkakas: os, processing (pemanggil alat Processing), dan banyak perkakas QGIS. |
| 11-12 | FOLDER, JARAK_SEMPADAN = 25 | Folder kerja dan jarak sempadan dalam meter. Nama kotak dengan huruf besar menandakan nilai tetap, yang diatur di awal. |
| 14-16 | proyek.clear() dan setCrs | Peringatan: clear() mengosongkan seluruh proyek. Lalu atur CRS proyek ke EPSG:32749. |
| 18-21 | def muat(...) dengan assert | Fungsi pemuat. Beda dengan Skrip 17.1, ia hanya membuat layer dan tidak menambahkannya ke proyek. Baris 20: bila layer.isValid() salah, skrip berhenti dengan pesan Layer tidak terbaca: diikuti nama layer. |
| 24-25 | mentah = muat(...), print(... mapUnits()) | Muat titik panas berderajat, lalu cetak kode CRS dan satuan petanya. |
| 28-31 | processing.run("native:reprojectlayer", {...}) | Panggil alat reproject. Isian pertama nama alat, isian kedua kamus pengaturan: layer masukan, CRS tujuan, dan OUTPUT berupa "memory:Titik_Panas", yaitu layer sementara di memori. Hasilnya kamus, dan layer baru diambil dengan ["OUTPUT"]. |
| 34-35 | petak = muat(...), sungai = muat(...) | Muat petak dan sungai. |
| 38 | petak.selectByExpression('"Jenis" = \'Jati\' AND "Volume_m3ha" > 150') | Pilih petak yang jenisnya Jati dan volumenya lebih dari 150. Seleksi ini hanya menandai petak, tanpa mengubah data. |
| 39-40 | print(... sorted(... for f in petak.selectedFeatures())), removeSelection() | Cetak kode petak terpilih, urut abjad. Lalu hapus seleksi. |
| 43-45 | sempadan = processing.run("native:buffer", {...})["OUTPUT"] | Buat buffer sungai sejauh JARAK_SEMPADAN. SEGMENTS: 16 mengatur kehalusan sudut, dan DISSOLVE: True menggabungkan semua buffer menjadi satu bidang. |
| 46-47 | luas_sempadan = sum(... for f in ...), print(...) | Jumlahkan luas bidang buffer, lalu cetak dalam meter persegi dan hektare. Satu hektare adalah 10.000 meter persegi. |
| 50-51 | petak_sempadan = processing.run("native:clip", {...})["OUTPUT"] | Potong petak dengan bidang buffer. Yang tersisa hanya bagian petak di dalam sempadan. |
| 52-57 | total = 0 dan perulangan for | Siapkan penjumlah total bernilai 0. Ulangi untuk tiap potongan, urut kode petak. Tambahkan luasnya ke total (total += luas), lalu cetak satu baris. Terakhir cetak jumlahnya. |
| 53 | key=lambda x: x["ID_Petak"] | Aturan pengurutan: urutkan menurut kolom ID_Petak. lambda adalah fungsi mini satu baris (Bab 7). |
| 60-62 | titik_dalam = processing.run("native:clip", ...) | Potong titik panas dengan buffer yang sama, lalu cetak jumlahnya. |
| 65-73 | petak.setRenderer(...) dan empat baris serupa | Beri simbol tunggal pada lima layer. Pola sama dengan Skrip 17.1. |
| 74-75 | for layer in (...): proyek.addMapLayer(layer) | Tambahkan lima layer ke proyek. Urutan penambahan menentukan tumpukan: yang terakhir berada di atas. |
| 76-78 | petak_sempadan.setName(...), sempadan.setName(...), titik.setName(...) | Beri nama ramah pada tiga layer sementara. Nama sempadan memakai %g, yaitu format angka yang mencetak 25 sebagai 25. Nilainya diambil dari JARAK_SEMPADAN, jadi nama ikut berubah bila jarak diganti. |
| 80-117 | mm = ... sampai tata.addLayoutItem(skala) | Layout ringkas: peta, judul, legenda, dan batang skala. Pola sama dengan Skrip 17.5. Perbedaannya, legenda tidak dibuat manual (baris 103-107), dan tidak ada panah utara atau catatan. Judul peta (baris 95) juga memakai %g dan JARAK_SEMPADAN. |
| 118-119 | hasil_pdf = QgsLayoutExporter(tata).exportToPdf(...) | Ekspor PDF, lalu cetak angka hasil. |
| 120 | proyek.write(os.path.join(FOLDER, "KPH_Contoh_B4.qgz")) | Simpan proyek ke berkas .qgz. |
| 121 | print("Proyek disimpan:", os.path.exists(...)) | Periksa bahwa berkas ada. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
CRS titik panas mentah: EPSG:4326 | satuan: 6
Titik panas setelah reproject: 12 titik, EPSG:32749
Jati dengan volume > 150 m3/ha: ['G05', 'G06', 'G07']
Luas buffer sungai: 29435 m2 (2.94 ha)
A04 Jati 739 m2
B01 Mahoni 1300 m2
B02 Agroforestri 5736 m2
B03 Akasia 5663 m2
B04 Sengon 203 m2
G01 Mahoni 134 m2
G02 Sengon 5690 m2
G03 Jati 558 m2
G05 Jati 5612 m2
G06 Jati 1438 m2
Total petak dalam sempadan: 27072 m2 (2.71 ha), 10 petak
Titik panas di dalam sempadan: 6 dari 12
Ekspor PDF: 0 (0 = berhasil)
Proyek disimpan: TrueCara membaca hasil. Angka 4326 adalah kode lintang-bujur (derajat).
Angka satuan: 6 adalah kode satuan peta untuk derajat. Uji saya: int(Qgis.DistanceUnit.Degrees) bernilai 6. Setelah reproject, 12 titik panas tetap 12, tetapi sistem koordinatnya UTM. Sepuluh dari 16 petak tersentuh sempadan, dengan luas total 27.072 meter persegi.
Bagian yang boleh diubah.
- Baris 11, folder. Ganti jalur komputer Anda.
- Baris 12, jarak sempadan. Ganti 25 menjadi angka lain. Nama layer sempadan dan judul peta ikut berubah. Komentar dan teks cetak lain tidak ikut (lihat Jebakan).
- Baris 38, ambang volume. Ganti
150, atau ganti'Jati'dengan jenis lain. - Baris 44, jumlah segmen. Ganti
16. Angka kecil membuat ujung buffer bersudut kasar. - Baris 65-73, warna simbol. Ganti kode warna.
- Baris 90-91, ekstensi dan skala peta. Sama seperti Skrip 17.5.
Jebakan.
proyek.clear()mengosongkan proyek. Lihat kotak peringatan di awal skrip ini. Hasil uji: tujuh layer dan layoutPeta_KPH_A4hilang, diganti lima layer baru dan layoutPeta_Sempadan.- Layer sementara tidak ikut tersimpan. Baris 120 menyimpan proyek, tetapi tiga layer hasil Processing hanya ada di memori. Saya menyimpan proyek lalu membukanya lagi di sesi baru. Hasilnya:
Petak di dalam sempadan,Sempadan sungai 25 m, danTitik panasikut muncul, tetapi ketiganya kosong (0 fitur). HanyaPetak KPHdanSungaiyang berisi. Untuk menyimpan hasil, tulis keluaran ke berkas, misalnya"OUTPUT": FOLDER + "/Sempadan.gpkg"pada baris 45. - Jalur salah menghentikan skrip dengan pesan jelas. Baris 20 menghasilkan
AssertionError: Layer tidak terbaca: Hotspot_mentah. Pesan di Skrip 17.1 menyebut nama berkas, sedangkan di sini nama layer. PeriksaFOLDERdan nama berkas. - Nama layer dan judul mengikuti angka, teks lain tidak. Ganti
JARAK_SEMPADANmenjadi 50. Layer lalu bernamaSempadan sungai 50 m(baris 77) dan judul peta berbunyi(CONTOH 50 M)(baris 95), karena keduanya memakai%g. Komentar di baris 42 yang menyebut "25 m" tetap, jadi ubah sendiri bila perlu. - Teks cetak tetap. Baris 39 mencetak
volume > 150sebagai teks tetap. Bila Anda mengubah ambang di baris 38, teksnya tidak ikut.
Latihan variasi. Lebarkan sempadan menjadi 50 meter, lalu bandingkan luas hasilnya.
Contoh jawaban. Ubah baris 12:
JARAK_SEMPADAN = 50 # meter; angka latihan, bukan ketentuan resmiLayar menampilkan Luas buffer sungai: 62790 m2 (6.28 ha) dan Total petak dalam sempadan: 51831 m2 (5.18 ha), 10 petak. Jumlah titik panas dalam sempadan tetap 6 dari 12. Layer sempadan di panel bernama Sempadan sungai 50 m, dan judul peta berbunyi (CONTOH 50 M).
Cek paham #
- Apa beda penyaji Categorized dan Graduated?
- Mengapa Skrip 17.2 menyaring petak kosong sebelum menghitung kelas?
- Mengapa Skrip 17.5 aman dijalankan dua kali, padahal dua layout tidak boleh sama namanya?
- Apa yang terjadi bila
FOLDERdi Skrip 17.1 salah, dan baris mana yang menyebabkannya?
Jawaban.
- Categorized memberi satu warna untuk tiap nilai teks, misalnya tiap jenis tanaman. Graduated memberi satu warna untuk tiap rentang angka, misalnya tiap kelas volume.
- Quantile dan Natural Breaks membaca nilai kosong sebagai nol. Tanpa saringan, kelas pertama ikut tertarik ke nol, dan batas kelas menjadi tidak mencerminkan data.
- Baris 14-16 Skrip 17.5 mencari layout lama bernama
Peta_KPH_A4dan membuangnya sebelum layout baru dibuat. Jadi tidak pernah ada dua layout sama nama. - Skrip berhenti dengan
AssertionError: Layer tidak terbaca:diikuti nama berkas. Penyebabnya baris 13 (assert layer.isValid()), yang memeriksa bahwa data berhasil dibaca.
Kesalahan umum #
- Menjalankan skrip tidak berurutan. Skrip 17.2, 17.4, dan 17.5 mengandalkan layer dari skrip sebelumnya. Perbaikan: ikuti tabel urutan di awal bab.
- Membiarkan jalur contoh.
FOLDERmenunjuk folder di komputer penulis. Perbaikan: ganti jalur di tiap skrip. Bila munculAssertionErrorberpesan "tidak terbaca", periksa jalur dan nama berkas. - Menjalankan Skrip 17.7 di proyek yang berisi pekerjaan. Perbaikan: simpan proyek dulu, atau pakai proyek kosong.
- Mengira hasil ekspor berhasil karena berkas ada. Perbaikan: baca angka
hasil. Nilai 0 berarti berhasil.
Ringkasan dan latihan #
- Peta jadi dirakit bertahap: layer, simbologi, label, layout, ekspor.
- Categorized mewarnai menurut nilai teks, Graduated menurut rentang angka, dan tiga metode kelas memberi batas berbeda.
- Layout tersusun dari item yang diletakkan dengan milimeter. Atur ukuran dulu, lalu cakupan, lalu skala.
- Ekspor PDF tetap tajam. PNG bergantung dpi. Selalu baca angka
hasil. - Baris
assertmenghentikan skrip dengan pesan jelas bila data atau layout tidak ada. - Dua hal yang perlu diingat: legenda manual hanya mengganti tulisan, bukan nama layer, dan
proyek.clear()mengosongkan proyek.
Latihan. Buat salinan Skrip 17.5 dengan layout bernama Peta_Volume_A4, skala 1:3500, dan judul "PETA VOLUME KAYU". Ekspor PDF-nya saja. Bagian mana yang Anda ubah, dan bagian mana yang Anda hapus?
Tabel perbandingan: empat cara menata tampilan peta #
| Pekerjaan | Perkakas di skrip | Isian utama | Skrip di bab ini |
|---|---|---|---|
| Warna per jenis | QgsCategorizedSymbolRenderer | kolom teks dan kategori | 17.1 |
| Warna per kelas angka | QgsGraduatedSymbolRenderer | kolom angka dan metode | 17.2 |
| Warna raster | QgsSingleBandPseudoColorRenderer | skala warna dan kelas | 17.3 |
| Tulisan di peta | QgsPalLayerSettings | ekspresi dan format teks | 17.4 |
| Lembar peta | QgsPrintLayout | item, letak, ukuran (mm) | 17.5 dan 17.7 |
| Berkas siap edar | QgsLayoutExporter | nama layout dan dpi | 17.6 |