BAB 23: Menyatukan Semuanya: Model, Proyek Terpadu, dan Ceklis Masuk M1
Studi kasus: "Sempadan 20, 30, atau 50 meter, lalu laporan awal bulan" #
Kepala Seksi belum memutuskan lebar sempadan sungai. "Coba 20 meter, 30 meter, lalu 50 meter, dan tunjukkan luasnya." Anda ingin satu mesin yang menerima lebar sebagai isian, dan menolak isian yang tidak masuk akal. Awal bulan tiba, dan ia minta laporan lengkap: kiriman lapangan, sempadan, titik panas, dan kondisi vegetasi. Tahun lalu pekerjaan itu memakan dua hari. Kali ini Anda ingin satu skrip yang menyelesaikannya, lalu satu ujian untuk mengetahui apakah Anda siap ke kelas berikutnya.
Konsep: model sebagai berkas, dalam tiga kalimat #
Model adalah alur kerja yang disimpan sebagai berkas .model3: kotak masukan, kotak alat, dan panah di antaranya. Bayangkan mesin cuci dengan tombol pengaturan. Mesinnya satu, tetapi hasilnya berbeda menurut pengaturan. Karena modelnya hanya berkas teks, ia bisa dibuat dengan klik di Model Designer, atau ditulis dengan Python seperti di bab ini.

Lima istilah baru:
- Model: alur kerja yang disimpan dalam berkas
.model3. - Masukan model: isian yang diisi pengguna saat model dijalankan.
- Langkah: satu alat di dalam model. Di API QGIS disebut child algorithm.
- Penjaga: langkah yang menghentikan model bila syarat gagal.
- Sumber nilai: asal isian sebuah langkah: nilai tetap, masukan model, keluaran langkah lain, atau hasil ekspresi.

Skrip 23.1: Membangun model lewat Python #
Tujuan. Menyusun model "Sempadan Sungai per KPH" dengan kode, lengkap dengan penjaga jarak, lalu menyimpannya sebagai .model3.
Bahan. Skrip ini tidak membaca data. Ia hanya menulis satu berkas, Sempadan_Sungai_per_KPH.model3, di folder HASIL. Berkas b6_01_buat_model.py (di dalamnya tertulis "Skrip 6.1"). Ganti HASIL di baris 11. Salinan jadi model ini juga ada di folder skrip/model pada Seri I4. Dijalankan di Editor Skrip QGIS, seperti di Bab 14.
Skrip ini bekerja dalam enam tahap. Gambar berikut menunjukkan urutannya, lengkap dengan nomor baris.

Skrip utuh:
# [SKRIP 6.1: Membangun model "Sempadan Sungai per KPH" lewat Python, simpan sebagai .model3]
# Penulis: Badar Mubarok Yogaswara
# Model ini sama dengan yang bisa Anda gambar di Graphical Modeler; ini hanya cara lain membuatnya.
import os
from qgis.core import (QgsProcessing, QgsProcessingModelAlgorithm, QgsProcessingModelChildAlgorithm,
QgsProcessingModelChildParameterSource as Sumber, QgsProcessingModelParameter,
QgsProcessingModelOutput, QgsProcessingModelChildDependency,
QgsProcessingParameterVectorLayer, QgsProcessingParameterNumber)
DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
os.makedirs(HASIL, exist_ok=True)
m = QgsProcessingModelAlgorithm("Sempadan Sungai per KPH", "Kehutanan")
# --- Masukan model (yang diisi pengguna saat model dijalankan)
m.addModelParameter(QgsProcessingParameterVectorLayer(
"KPH", "Layer batas KPH", [QgsProcessing.TypeVectorPolygon]), QgsProcessingModelParameter("KPH"))
m.addModelParameter(QgsProcessingParameterVectorLayer(
"SUNGAI", "Layer sungai", [QgsProcessing.TypeVectorLine]), QgsProcessingModelParameter("SUNGAI"))
m.addModelParameter(QgsProcessingParameterNumber(
"JARAK", "Lebar sempadan (m)", QgsProcessingParameterNumber.Double, 30),
QgsProcessingModelParameter("JARAK"))
def anak(alat, deskripsi):
c = QgsProcessingModelChildAlgorithm(alat)
c.generateChildId(m)
c.setDescription(deskripsi)
return c
def dari_input(nama):
return [Sumber.fromModelParameter(nama)]
def nilai(v):
return [Sumber.fromStaticValue(v)]
def dari_anak(c, keluaran="OUTPUT"):
return [Sumber.fromChildOutput(c.childId(), keluaran)]
# --- Penjaga: bila JARAK tidak lebih dari 0, model berhenti dengan pesan jelas
henti = anak("native:raiseexception", "Hentikan bila jarak tidak sah")
henti.addParameterSources("MESSAGE", nilai("Lebar sempadan harus lebih dari 0 meter."))
henti.addParameterSources("CONDITION", [Sumber.fromExpression("@JARAK <= 0")])
m.addChildAlgorithm(henti)
# --- Langkah utama: baru berjalan sesudah penjaga lolos
buf = anak("native:buffer", "Penyangga sungai")
buf.addParameterSources("INPUT", dari_input("SUNGAI"))
buf.addParameterSources("DISTANCE", dari_input("JARAK"))
buf.addParameterSources("DISSOLVE", nilai(True))
buf.setDependencies([QgsProcessingModelChildDependency(henti.childId())])
m.addChildAlgorithm(buf)
iris = anak("native:intersection", "Irisan dengan KPH")
iris.addParameterSources("INPUT", dari_input("KPH"))
iris.addParameterSources("OVERLAY", dari_anak(buf))
m.addChildAlgorithm(iris)
luas = anak("native:fieldcalculator", "Hitung luas (ha)")
luas.addParameterSources("INPUT", dari_anak(iris))
luas.addParameterSources("FIELD_NAME", nilai("LUAS_HA"))
luas.addParameterSources("FIELD_TYPE", nilai(0))
luas.addParameterSources("FIELD_LENGTH", nilai(10))
luas.addParameterSources("FIELD_PRECISION", nilai(2))
# rumus dihitung per fitur, jadi diberikan sebagai nilai tetap (teks)
luas.addParameterSources("FORMULA", nilai("round($area / 10000, 2)"))
keluaran = QgsProcessingModelOutput("SEMPADAN", "Sempadan per KPH")
keluaran.setChildOutputName("OUTPUT")
luas.setModelOutputs({"SEMPADAN": keluaran})
m.addChildAlgorithm(luas)
berkas = HASIL + "/Sempadan_Sungai_per_KPH.model3"
print("Model tersimpan?", m.toFile(berkas))
print("Langkah:", [c.description() for c in m.childAlgorithms().values()])
print("Masukan:", [p.name() for p in m.parameterDefinitions()])Penjelasan baris demi baris. Skrip ini 80 baris, jadi dibahas dalam tujuh blok. Nomor baris mengikuti berkas di atas.
Blok 1: pembuka (baris 1-14).
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-3 | # [SKRIP 6.1 ... | Komentar. Baris 3 menyebut bahwa model ini sama dengan yang bisa Anda gambar di Model Designer. |
| 4 | import os | Perkakas untuk urusan folder (Bab 8). |
| 5-8 | from qgis.core import (...) | Panggil perkakas pembuat model. QgsProcessingModelChildParameterSource as Sumber memberi nama panjang itu julukan pendek Sumber. Julukan seperti ini disebut alias. |
| 10-12 | DATA, HASIL, os.makedirs(...) | Dua alamat, lalu buat folder hasil bila belum ada. DATA tidak dipakai skrip ini. |
| 14 | m = QgsProcessingModelAlgorithm("Sempadan Sungai per KPH", "Kehutanan") | Buat model kosong di kotak m. Isian pertama nama model, isian kedua nama kelompoknya. |
Blok 2: tiga masukan (baris 16-23). Ini isian yang akan diisi pengguna saat model dijalankan.
| Baris | Tulisan | Artinya |
|---|---|---|
| 17-18 | m.addModelParameter(QgsProcessingParameterVectorLayer("KPH", ...), QgsProcessingModelParameter("KPH")) | Tambah masukan bernama KPH. Isiannya nama internal, label di jendela ("Layer batas KPH"), dan jenis layer yang diterima: poligon. Bagian kedua mendaftarkan masukan itu pada model. |
| 19-20 | masukan SUNGAI | Sama, tetapi menerima layer garis. |
| 21-23 | masukan JARAK | Masukan angka desimal (Double) dengan nilai bawaan 30. |
Blok 3: empat pembantu (baris 26-42). Empat fungsi kecil ini mempersingkat kode di bawahnya. Fungsi dibahas di Bab 7.
| Baris | Tulisan | Artinya |
|---|---|---|
| 26-30 | def anak(alat, deskripsi): | Buat satu langkah. generateChildId(m) memberinya nomor pengenal yang unik di model, dan setDescription memberi nama yang terbaca manusia. |
| 33-34 | def dari_input(nama): | Sumber nilai dari masukan model. Dikembalikan dalam daftar. |
| 37-38 | def nilai(v): | Sumber nilai berupa nilai tetap. |
| 41-42 | def dari_anak(c, keluaran="OUTPUT"): | Sumber nilai dari keluaran langkah lain. Isian kedua punya nilai bawaan "OUTPUT". |
Sumber keempat, hasil ekspresi, dipakai langsung di baris 48 lewat Sumber.fromExpression(...).
Blok 4: penjaga (baris 45-49).
| Baris | Tulisan | Artinya |
|---|---|---|
| 46 | henti = anak("native:raiseexception", ...) | Buat langkah dari alat Raise exception, yaitu alat yang sengaja menghentikan model dengan pesan buatan Anda. |
| 47 | henti.addParameterSources("MESSAGE", nilai(...)) | Isi pesannya dengan nilai tetap: "Lebar sempadan harus lebih dari 0 meter." |
| 48 | ... "CONDITION", [Sumber.fromExpression("@JARAK <= 0")] | Isi syaratnya dengan ekspresi. @JARAK membaca masukan model. Alat hanya menghentikan model bila syarat ini benar. |
| 49 | m.addChildAlgorithm(henti) | Pasang langkah ini ke model. |
Blok 5: buffer dan irisan (baris 51-62).
| Baris | Tulisan | Artinya |
|---|---|---|
| 52-55 | buf = anak("native:buffer", ...) lalu tiga addParameterSources | Buat langkah Buffer. INPUT diambil dari masukan SUNGAI, DISTANCE dari masukan JARAK, dan DISSOLVE bernilai tetap benar (hasil digabung). |
| 56 | buf.setDependencies([QgsProcessingModelChildDependency(henti.childId())]) | Syarat urutan: Buffer baru boleh jalan setelah penjaga lolos. Tanpa baris ini, QGIS tidak tahu bahwa penjaga harus lebih dulu. |
| 57 | m.addChildAlgorithm(buf) | Pasang ke model. |
| 59-62 | iris = anak("native:intersection", ...) | Langkah Irisan. INPUT dari masukan KPH, OVERLAY dari keluaran Buffer (dari_anak(buf)). |
Blok 6: luas dan keluaran (baris 64-75).
| Baris | Tulisan | Artinya |
|---|---|---|
| 64-69 | luas = anak("native:fieldcalculator", ...) | Langkah Field calculator. INPUT dari hasil irisan, lalu nama kolom LUAS_HA, jenis desimal (0), panjang 10, dua desimal. |
| 70-71 | "FORMULA" bernilai tetap | Rumus round($area / 10000, 2) diberikan sebagai teks tetap, bukan hasil ekspresi. Rumus per fitur harus dihitung oleh alatnya sendiri untuk tiap fitur. Hasil ekspresi dihitung sekali untuk seluruh model, tanpa fitur. |
| 72-74 | keluaran = QgsProcessingModelOutput("SEMPADAN", "Sempadan per KPH") | Tandai hasil langkah ini sebagai keluaran model. setChildOutputName("OUTPUT") menyebut keluaran alat mana yang dipakai, dan setModelOutputs memasangnya. |
| 75 | m.addChildAlgorithm(luas) | Pasang langkah terakhir. |
Blok 7: simpan dan lapor (baris 77-80).
| Baris | Tulisan | Artinya |
|---|---|---|
| 77-78 | m.toFile(berkas) | Simpan model ke berkas .model3. Hasilnya True bila berhasil. |
| 79 | [c.description() for c in m.childAlgorithms().values()] | Cetak nama semua langkah. .values() mengambil isi kamus langkah. |
| 80 | [p.name() for p in m.parameterDefinitions()] | Cetak nama semua isian model. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Model tersimpan? True
Langkah: ['Penyangga sungai', 'Hitung luas (ha)', 'Irisan dengan KPH', 'Hentikan bila jarak tidak sah']
Masukan: ['KPH', 'SUNGAI', 'JARAK', 'sempadan_per_kph']Ada tiga hal yang perlu Anda baca dengan teliti. Pertama, Model tersimpan? True berarti berkas sudah tertulis. Kedua, urutan langkah yang dicetak tidak sama dengan urutan Anda menyusunnya: penjaga muncul paling akhir. Urutan jalan model ditentukan oleh sumber nilai dan ketergantungan, bukan urutan cetak. Ketiga, daftar masukan memuat empat nama. Yang keempat, sempadan_per_kph, adalah keluaran model yang diperlakukan sebagai isian (tempat menyimpan hasil). Namanya dibentuk dari label keluaran di baris 72, yaitu "Sempadan per KPH", dengan huruf kecil dan garis bawah. Pada uji, berkas hasil skrip ini identik dengan salinan di folder skrip/model Seri I4.
Bagian yang boleh diubah.
- Baris 11, lokasi hasil. Ganti dengan folder Anda. Akibatnya, berkas model tersimpan di sana.
- Baris 14, nama model dan kelompok. Nama ini tampil di Processing Toolbox bila model dipasang.
- Baris 22, angka 30. Nilai bawaan lebar sempadan.
- Baris 47, pesan penjaga. Ganti kalimatnya. Akibatnya, pesan itu muncul bila jarak tidak sah.
- Baris 48, syarat.
@JARAK <= 0boleh diganti, misalnya@JARAK <= 0 or @JARAK > 500. - Baris 55,
nilai(True). GantiFalsebila potongan buffer tidak perlu digabung.
Jebakan.
- Nama keluaran bergantung pada label. Pada uji, label di baris 72 diganti dari "Sempadan per KPH" menjadi "Sempadan Akhir". Daftar masukan lalu memuat
sempadan_akhir. Skrip 23.2, yang masih memakaisempadan_per_kph, berhenti denganCould not create destination layer for sempadan_akhir: no value specified for parameter. Bila Anda mengganti label, ganti juga nama itu di Skrip 23.2. - Urutan cetak bukan urutan jalan. Lihat penjelasan Hasil di atas. Jangan menebak urutan model dari daftar
Langkah. DATAtidak dipakai. Baris 10 menyiapkan alamat yang tidak terpakai. Tidak merusak, tetapi bisa membingungkan.- Memberi rumus per fitur sebagai ekspresi model. Dibahas di Blok 6. Buku I4 mencatat bahwa dalam ujinya, rumus itu bila diberikan sebagai hasil ekspresi membuat langkah gagal dengan pesan
Error encountered while running Hitung luas (ha). Itu sebabnyaFORMULAberupa nilai tetap. [CEK: bagian ini tidak diuji ulang di bab ini]
Latihan variasi.
- Ganti pesan penjaga menjadi "Jarak harus lebih besar dari nol."
- Ubah nilai bawaan
JARAKmenjadi 50.
Contoh jawaban nomor 1 (sudah diuji): ubah baris 47 menjadi
henti.addParameterSources("MESSAGE", nilai("Jarak harus lebih besar dari nol."))Jalankan ulang Skrip 23.1, lalu Skrip 23.2. Pada baris terakhir hasil, pesan penghentian berubah menjadi Jarak harus lebih besar dari nol.
Contoh jawaban nomor 2: ubah angka 30 di baris 22 menjadi 50. Nilai itu menjadi isian awal saat model dibuka dari menu. Skrip 23.2 mengisi JARAK sendiri, jadi hasilnya tidak berubah.
Skrip 23.2: Menjalankan model dari Python #
Tujuan. Memuat model dari berkas, menjalankannya tiga kali dengan lebar sempadan berbeda, lalu menguji penjaga dengan jarak yang tidak sah.
Bahan. Paket paket-i4: Batas_KPH.gpkg dan Sungai.gpkg, serta berkas model dari Skrip 23.1. Jalankan Skrip 23.1 lebih dulu. Berkas b6_02_jalankan_model.py (di dalamnya tertulis "Skrip 6.2"). Ganti DATA di baris 7 dan HASIL di baris 8.

Skrip utuh:
# [SKRIP 6.2: Menjalankan model .model3 dari Python, berulang dengan jarak berbeda]
# Penulis: Badar Mubarok Yogaswara
# Prasyarat: jalankan SKRIP 6.1 dulu (atau simpan model dari Graphical Modeler dengan nama sama).
import processing
from qgis.core import QgsVectorLayer, QgsProcessingModelAlgorithm, QgsProcessingException
DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
sungai = QgsVectorLayer(DATA + "/Sungai.gpkg", "Sungai", "ogr")
# 1. Muat model dari berkas
model = QgsProcessingModelAlgorithm()
print("Model terbaca?", model.fromFile(HASIL + "/Sempadan_Sungai_per_KPH.model3"))
model.initAlgorithm({})
print("Nama model:", model.displayName())
print("Parameter:", [p.name() for p in model.parameterDefinitions()])
# 2. Jalankan berulang dengan tiga lebar sempadan. Model yang sama, angka berbeda.
for jarak in (20, 30, 50):
hasil = processing.run(model, {
"KPH": batas, "SUNGAI": sungai, "JARAK": jarak,
"sempadan_per_kph": "TEMPORARY_OUTPUT"})
lyr = hasil["sempadan_per_kph"]
total = sum(f["LUAS_HA"] for f in lyr.getFeatures())
print("Lebar %2d m -> %d potongan, total %.2f ha" % (jarak, lyr.featureCount(), total))
# 3. Jarak yang tidak sah: model berhenti dan memberi pesan
try:
processing.run(model, {"KPH": batas, "SUNGAI": sungai, "JARAK": 0,
"sempadan_per_kph": "TEMPORARY_OUTPUT"})
print("Jarak 0: model berjalan (tidak seharusnya)")
except QgsProcessingException as e:
print("Jarak 0: model berhenti ->", str(e).replace("\n", " | "))Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 4-5 | import processing dan from qgis.core import ... | Perkakas untuk memanggil alat, membuka layer, memuat model, dan menangkap QgsProcessingException. |
| 10-11 | batas = ... dan sungai = ... | Buka dua layer masukan. |
| 14-15 | model = QgsProcessingModelAlgorithm() lalu model.fromFile(...) | Buat model kosong, lalu isi dari berkas. fromFile menjawab True bila berkas terbaca, dan False bila tidak. |
| 16 | model.initAlgorithm({}) | Siapkan model supaya isiannya terdaftar. Tanpa baris ini, daftar isian kosong. |
| 17-18 | displayName() dan parameterDefinitions() | Cetak nama model dan nama semua isiannya. |
| 21 | for jarak in (20, 30, 50): | Ulangi tiga kali, dengan jarak berturut-turut 20, 30, dan 50. Tanda kurung di sini membuat tuple (Bab 5). |
| 22-24 | processing.run(model, {...}) | Jalankan model. Bedanya dengan Bab 21: isian pertama adalah objek model, bukan teks ID alat. Kamusnya berisi KPH, SUNGAI, JARAK, dan tempat keluaran. |
| 25 | lyr = hasil["sempadan_per_kph"] | Ambil layer hasil memakai nama keluaran yang Anda lihat di Skrip 23.1. Huruf kecilnya harus persis. |
| 26 | total = sum(f["LUAS_HA"] for f in lyr.getFeatures()) | Jumlahkan kolom LUAS_HA dari semua potongan. |
| 27 | print("Lebar %2d m -> %d potongan, total %.2f ha" % ...) | Cetak lebar, jumlah potongan, dan total luas. %2d bilangan bulat dua kolom, %.2f desimal dua angka. |
| 30-35 | try: ... except QgsProcessingException as e: | Jalankan model dengan JARAK 0. Penjaga menghentikan model, dan except menangkap pesannya. .replace("\n", ...) merapikan pesan. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Model terbaca? True
Nama model: Sempadan Sungai per KPH
Parameter: ['JARAK', 'KPH', 'SUNGAI', 'sempadan_per_kph']
Lebar 20 m -> 3 potongan, total 2.75 ha
Lebar 30 m -> 3 potongan, total 4.07 ha
Lebar 50 m -> 3 potongan, total 6.55 ha
Jarak 0: model berhenti -> Lebar sempadan harus lebih dari 0 meter.Semakin lebar sempadan, semakin besar luasnya: 2,75 ha untuk 20 meter, 4,07 ha untuk 30 meter, dan 6,55 ha untuk 50 meter. Selalu tiga potongan, satu per KPH. Total 4,07 ha untuk 30 meter sama dengan jumlah sempadan per KPH di Skrip 23.3 (1,18 + 1,39 + 1,50). Baris terakhir membuktikan penjaga bekerja: jarak 0 ditolak dengan pesan yang Anda tulis sendiri di Skrip 23.1.
Bagian yang boleh diubah.
- Baris 7-8, jalur. Sesuaikan dengan komputer Anda.
- Baris 15, nama berkas model. Ganti untuk memuat model lain, misalnya model buatan Anda di Model Designer.
- Baris 21, daftar jarak.
(20, 30, 50)boleh diganti angka lain. Akibatnya, jumlah putaran dan hasilnya berubah. - Baris 23-24, nama isian. Harus sama dengan nama di daftar
Parameter:pada hasil. - Baris 31, jarak 0. Ganti angka lain untuk menguji batas.
Jebakan.
- Lupa menjalankan Skrip 23.1. Pada uji, berkas model dihapus lebih dulu. Layar menampilkan
Model terbaca? False,Nama model: model, danParameter: [], lalu skrip berhenti denganKeyError: 'sempadan_per_kph'di baris 25. Baris pertama hasil sudah memberi tahu, sebelum galatnya muncul. - Huruf besar dan kecil. Pada uji,
"sempadan_per_kph"di baris 25 ditulis"Sempadan_per_KPH". HasilnyaKeyError: 'Sempadan_per_KPH'. Salin nama dari daftarParameter:. - Keluaran tampak sebagai isian. Daftar
Parameter:memuatsempadan_per_kphwalau itu hasil. Di API QGIS, tempat penyimpanan hasil memang dihitung sebagai parameter. - Urutan daftar berbeda. Daftar di sini urut abjad (
JARAK,KPH,SUNGAI), sedangkan di Skrip 23.1 urut buatan. Isinya sama.
Latihan variasi.
- Ganti daftar jarak menjadi 10, 25, 40, dan 100.
- Cetak juga luas sempadan per KPH di tiap putaran.
Contoh jawaban nomor 1 (sudah diuji): ubah baris 21 menjadi for jarak in (10, 25, 40, 100):. Layar menampilkan total 1,40 ha, 3,41 ha, 5,32 ha, dan 11,50 ha.
Contoh jawaban nomor 2 (sudah diuji): sisipkan sebelum baris 27.
for f in lyr.getFeatures():
print(" ", f["NAMA_KPH"], f["LUAS_HA"])Di putaran 30 meter, layar menambah KPH Alpha 1.18, KPH Beta 1.39, dan KPH Gamma 1.5.
Konsep: proyek terpadu dan ceklis #
Proyek terpadu adalah satu skrip yang memadukan semua yang Anda pelajari di kelas ini. Bayangkan ujian praktik sebelum turun ke jalan raya: yang diuji tangan Anda, bukan hanya ingatan. Ceklis adalah daftar pemeriksaan yang memberi tahu apakah bekal Anda sudah cukup untuk kelas berikutnya.
Dua istilah ikut muncul: keluaran permanen, yaitu hasil yang disimpan ke berkas, dan ceklis masuk, yaitu daftar kemampuan yang dianggap sudah Anda kuasai sebelum masuk M1. M1 adalah Masterclass GIS Modern, yang skripnya dibahas mulai Bab 24.
Skrip 23.3: Proyek mini terpadu #
Tujuan. Menjalankan satu alur dari kiriman lapangan sampai tabel ringkasan per KPH, lalu menyimpan hasilnya ke GeoPackage dan proyek QGIS.
Bahan. Seluruh paket paket-i4. Berkas b7_01_proyek_terpadu.py (di dalamnya tertulis "Skrip 7.1"). Ganti DATA di baris 10 dan HASIL di baris 11. Skrip menghasilkan lima berkas di folder HASIL: Proyek_Terpadu.qgz, Hasil_Terpadu.gpkg, ringkasan_kph.csv, log_kiriman.csv, serta NDVI_KPH.tif dan CHM.tif. Jalankan di proyek kosong. Baris 17-19 mengosongkan dan mengubah proyek yang terbuka.
Skrip ini 149 baris dan hampir tidak punya hal yang benar-benar baru. Ia memadukan Skrip 22.2 (data lapangan), Bab 21 (alat vektor dan raster), dan Skrip 22.4 (ekspresi). Karena itu penjelasannya dibagi delapan blok, dan setiap blok menyebut asal pola yang dipakai.

Skrip utuh:
# [SKRIP 7.1: Proyek mini terpadu I1-I3: dari kiriman lapangan sampai ringkasan per KPH]
# Penulis: Badar Mubarok Yogaswara
# Keluaran di folder HASIL: Proyek_Terpadu.qgz, Hasil_Terpadu.gpkg, ringkasan_kph.csv, log_kiriman.csv.
import csv
import glob
import os
import processing
from qgis.core import QgsProject, QgsVectorLayer, QgsRasterLayer, QgsCoordinateReferenceSystem
DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
os.makedirs(HASIL, exist_ok=True)
CRS = "EPSG:32749"
SEMENTARA = "TEMPORARY_OUTPUT"
proyek = QgsProject.instance()
proyek.removeAllMapLayers()
proyek.setCrs(QgsCoordinateReferenceSystem(CRS))
proyek.setTitle("KPH Contoh - proyek terpadu")
# ------------------------------------------------------------------ bagian 0: pembantu
def muat_vektor(jalur, nama):
lyr = QgsVectorLayer(jalur, nama, "ogr")
if not lyr.isValid():
raise ValueError("vektor tidak bisa dibuka: " + jalur)
return lyr
def muat_raster(jalur, nama):
lyr = QgsRasterLayer(jalur, nama, "gdal")
if not lyr.isValid():
raise ValueError("raster tidak bisa dibuka: " + jalur)
return lyr
def jalankan(alat, **parameter):
"""Menjalankan alat Processing lalu mengembalikan isi kunci OUTPUT."""
return processing.run(alat, parameter)["OUTPUT"]
def hitung_kolom(layer, nama, tipe, rumus):
"""Menambah satu kolom dengan ekspresi. tipe: 0 = desimal, 1 = bilangan bulat."""
return jalankan("native:fieldcalculator", INPUT=layer, FIELD_NAME=nama, FIELD_TYPE=tipe,
FIELD_LENGTH=10, FIELD_PRECISION=2 if tipe == 0 else 0, FORMULA=rumus,
OUTPUT=SEMENTARA)
batas = muat_vektor(DATA + "/Batas_KPH.gpkg", "Batas_KPH")
sungai = muat_vektor(DATA + "/Sungai.gpkg", "Sungai")
hotspot = muat_vektor(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional")
citra = muat_raster(DATA + "/Citra_KPH.tif", "Citra_KPH")
dsm = muat_raster(DATA + "/DSM_Drone.tif", "DSM")
dtm = muat_raster(DATA + "/DTM_Drone.tif", "DTM")
for lyr in (batas, sungai, hotspot, citra):
proyek.addMapLayer(lyr)
# ------------------------------------------------------------------ bagian I3: data lapangan
tujuan_crs = QgsCoordinateReferenceSystem(CRS)
diterima, log = [], []
for jalur in sorted(glob.glob(DATA + "/Kiriman_Lapangan/*.gpkg")):
nama = os.path.basename(jalur)
try:
lyr = muat_vektor(jalur, nama)
kurang = [f for f in ("Kondisi", "Tinggi_Phn", "Foto") if lyr.fields().indexOf(f) < 0]
if kurang:
raise ValueError("field hilang: " + ", ".join(kurang))
if lyr.crs().authid() != CRS:
lyr = jalankan("native:reprojectlayer", INPUT=lyr, TARGET_CRS=tujuan_crs, OUTPUT=SEMENTARA)
diterima.append(lyr)
log.append((nama, "DITERIMA"))
except Exception as e:
log.append((nama, "DITOLAK: %s" % e))
gabung = jalankan("native:mergevectorlayers", LAYERS=diterima, CRS=tujuan_crs, OUTPUT=SEMENTARA)
# Tiap berkas membawa nomor fid sendiri, jadi setelah digabung ada fid kembar. Buang kolom itu;
# GeoPackage akan memberi nomor baru yang unik saat disimpan.
survei = jalankan("native:deletecolumn", INPUT=gabung, COLUMN=["fid"], OUTPUT=SEMENTARA)
survei.setName("Titik_Survei_Gabungan")
proyek.addMapLayer(survei)
with open(HASIL + "/log_kiriman.csv", "w", newline="", encoding="utf-8") as f:
csv.writer(f).writerows([("berkas", "status")] + log)
ditolak = sum(1 for _, s in log if s != "DITERIMA")
print("Kiriman:", len(log) - ditolak, "diterima,", ditolak, "ditolak;",
"titik gabungan =", survei.featureCount())
# ------------------------------------------------------------------ bagian I1: analisis vektor
sempadan = jalankan("native:buffer", INPUT=sungai, DISTANCE=30, DISSOLVE=True, OUTPUT=SEMENTARA)
sempadan.setName("Sempadan_30m")
proyek.addMapLayer(sempadan)
irisan = jalankan("native:intersection", INPUT=batas, OVERLAY=sempadan, INPUT_FIELDS=["NAMA_KPH"],
OVERLAY_FIELDS=[], OUTPUT=SEMENTARA)
sempadan_ha = {}
for f in irisan.getFeatures():
sempadan_ha[f["NAMA_KPH"]] = sempadan_ha.get(f["NAMA_KPH"], 0) + f.geometry().area() / 10000
# Kolom baru lewat ekspresi: luas, titik panas, titik survei (agregasi dari layer lain)
cacah = "aggregate('%s', 'count', \"%s\", intersects($geometry, geometry(@parent)))"
ringkas = hitung_kolom(batas, "LUAS_HA", 0, "round($area / 10000, 2)")
ringkas = hitung_kolom(ringkas, "HOTSPOT", 1, cacah % ("Hotspot_Nasional", "ID_Titik"))
ringkas = hitung_kolom(ringkas, "TITIK_SURVEI", 1, cacah % ("Titik_Survei_Gabungan", "Foto"))
ringkas.setName("Ringkasan_KPH")
proyek.addMapLayer(ringkas)
# ------------------------------------------------------------------ bagian I2: analisis raster
ndvi_jalur = jalankan("native:rastercalc", LAYERS=[citra], OUTPUT=HASIL + "/NDVI_KPH.tif",
EXPRESSION='("Citra_KPH@4" - "Citra_KPH@1") / ("Citra_KPH@4" + "Citra_KPH@1")')
chm_jalur = jalankan("native:rastercalc", LAYERS=[dsm, dtm], OUTPUT=HASIL + "/CHM.tif",
EXPRESSION='"DSM@1" - "DTM@1"')
ndvi = muat_raster(ndvi_jalur, "NDVI_KPH")
chm = muat_raster(chm_jalur, "CHM")
proyek.addMapLayer(ndvi)
proyek.addMapLayer(chm)
def rata(raster, awalan):
z = jalankan("native:zonalstatisticsfb", INPUT=batas, INPUT_RASTER=raster, RASTER_BAND=1,
COLUMN_PREFIX=awalan, STATISTICS=[2], OUTPUT=SEMENTARA)
return {f["NAMA_KPH"]: f[awalan + "mean"] for f in z.getFeatures()}
ndvi_rata, chm_rata = rata(ndvi, "n_"), rata(chm, "c_")
# ------------------------------------------------------------------ bagian akhir: tabel dan proyek
judul = ["NAMA_KPH", "LUAS_HA", "SEMPADAN_HA", "HOTSPOT", "TITIK_SURVEI", "NDVI_RATA", "CHM_RATA_M"]
baris = []
for f in ringkas.getFeatures():
n = f["NAMA_KPH"]
baris.append([n, f["LUAS_HA"], round(sempadan_ha.get(n, 0), 2), f["HOTSPOT"], f["TITIK_SURVEI"],
round(ndvi_rata[n], 3), round(chm_rata[n], 2)])
with open(HASIL + "/ringkasan_kph.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(judul)
w.writerows(baris)
print("%-10s %7s %12s %8s %13s %10s %11s" % tuple(judul))
for b in baris:
print("%-10s %7.2f %12.2f %8d %13d %10.3f %11.2f" % tuple(b))
# Layer sementara hilang saat QGIS ditutup. Simpan yang penting ke satu GeoPackage,
# lalu ganti layer sementara di proyek dengan layer dari GeoPackage itu.
paket = HASIL + "/Hasil_Terpadu.gpkg"
processing.run("native:package", {"LAYERS": [survei, sempadan, ringkas], "OUTPUT": paket,
"OVERWRITE": True})
for lyr in (survei, sempadan, ringkas):
nama = lyr.name()
proyek.removeMapLayer(lyr.id())
proyek.addMapLayer(muat_vektor(paket + "|layername=" + nama, nama))
print("Proyek tersimpan?", proyek.write(HASIL + "/Proyek_Terpadu.qgz"))
print("Isi proyek:", sorted(l.name() for l in proyek.mapLayers().values()))Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.
Blok 0: menyiapkan proyek (baris 1-20).
| Baris | Tulisan | Artinya |
|---|---|---|
| 1-3 | komentar | Judul, penulis, dan daftar keluaran. |
| 4-8 | import ... dan from qgis.core import ... | Perkakas yang sudah Anda kenal: csv, glob, os, processing, serta layer vektor, layer raster, dan CRS. |
| 10-12 | DATA, HASIL, os.makedirs(...) | Dua alamat dan pembuat folder hasil. |
| 14-15 | CRS dan SEMENTARA | Dua nilai yang dipakai berulang, ditulis sekali di atas. Ganti di satu tempat, berlaku di semua tempat. |
| 16-19 | proyek = QgsProject.instance() dan tiga baris sesudahnya | Ambil proyek terbuka, kosongkan semua layernya (removeAllMapLayers), tetapkan CRS, dan beri judul. |
Blok 1: empat pembantu (baris 22-47). Pola dari Skrip 22.1 dan 22.3 dipakai lagi dalam bentuk fungsi.
| Baris | Tulisan | Artinya |
|---|---|---|
| 23-27 | def muat_vektor(jalur, nama): | Buka layer vektor. Bila tidak sah, raise ValueError. |
| 30-34 | def muat_raster(jalur, nama): | Sama untuk raster, dengan penyedia data "gdal". |
| 37-39 | def jalankan(alat, parameter): | Tanda parameter mengumpulkan semua isian bernama menjadi satu kamus (Bab 7). Fungsi lalu menjalankan alat dan langsung mengembalikan isi ["OUTPUT"]. Hasilnya, pemanggilan jadi pendek: jalankan("native:buffer", INPUT=..., DISTANCE=30). |
| 42-46 | def hitung_kolom(layer, nama, tipe, rumus): | Tambah satu kolom dengan Field calculator. tipe 0 berarti desimal dan 1 bulat. FIELD_PRECISION=2 if tipe == 0 else 0 memilih dua desimal untuk desimal, nol untuk bulat. |
Blok 2: memuat enam layer (baris 49-56).
| Baris | Tulisan | Artinya |
|---|---|---|
| 49-54 | enam pemanggilan muat_... | Buka tiga layer vektor (batas, sungai, titik panas) dan tiga raster (citra, DSM, DTM). Layer yang gagal dibuka menghentikan skrip dengan pesan jelas. |
| 55-56 | for lyr in (batas, sungai, hotspot, citra): | Hanya empat yang dimasukkan ke proyek. DSM dan DTM dipakai sebagai bahan hitung, tidak ditampilkan. |
Blok 3: data lapangan (baris 58-85). Ini Skrip 22.2 yang diperpendek.
| Baris | Tulisan | Artinya |
|---|---|---|
| 59-60 | tujuan_crs = ... dan diterima, log = [], [] | Siapkan CRS tujuan dan dua daftar kosong sekaligus dalam satu baris. |
| 61-73 | for jalur in sorted(glob.glob(...)): | Untuk tiap kiriman: buka dengan muat_vektor, periksa tiga kolom wajib, ubah CRS bila beda, lalu catat DITERIMA atau DITOLAK. Perbedaan dari Skrip 22.2: tidak ada pemeriksaan titik di luar KPH. |
| 75 | gabung = jalankan("native:mergevectorlayers", ...) | Gabungkan semua kiriman yang diterima. |
| 76-78 | komentar dan native:deletecolumn | Tiap berkas membawa nomor fid sendiri, jadi setelah digabung ada fid kembar. Kolom itu dibuang, dan GeoPackage akan memberi nomor baru yang unik saat menyimpan. |
| 79-80 | survei.setName(...) lalu addMapLayer | Beri nama Titik_Survei_Gabungan dan tampilkan. Nama ini dipakai rumus di baris 101. |
| 81-82 | with open(... "log_kiriman.csv" ...) | Tulis log. [("berkas", "status")] + log menyambung satu baris judul di depan daftar log. |
| 83 | ditolak = sum(1 for _, s in log if s != "DITERIMA") | Hitung catatan yang bukan DITERIMA. Garis bawah _ adalah nama kotak untuk nilai yang tidak dipakai. |
| 84-85 | print("Kiriman:", ...) | Cetak ringkasan diterima dan ditolak. |
Blok 4: analisis vektor (baris 87-103). Pola dari Bab 21 dan Skrip 22.4.
| Baris | Tulisan | Artinya |
|---|---|---|
| 88-90 | sempadan = jalankan("native:buffer", ...) | Sempadan 30 meter di kiri-kanan sungai, digabung jadi satu (DISSOLVE=True). Beri nama Sempadan_30m dan tampilkan. |
| 91-92 | irisan = jalankan("native:intersection", ...) | Iris batas KPH dengan sempadan. INPUT_FIELDS=["NAMA_KPH"] membawa kolom nama KPH saja, dan OVERLAY_FIELDS=[] tidak membawa kolom dari sempadan. |
| 93-95 | sempadan_ha = {} lalu perulangan | Jumlahkan luas irisan per KPH (dalam hektare). sempadan_ha.get(nama, 0) mengambil jumlah sementara, atau 0 bila KPH belum tercatat. Perlu dijumlah karena satu KPH bisa punya beberapa potongan. |
| 98 | cacah = "aggregate('%s', 'count', \"%s\", intersects(...))" | Cetakan rumus agregasi. Dua tanda %s nanti diisi nama layer dan nama kolom. |
| 99-101 | tiga hitung_kolom(...) berantai | Tambah kolom LUAS_HA, HOTSPOT, dan TITIK_SURVEI. Hasil tiap langkah menjadi masukan langkah berikutnya, dan cacah % (...) mengisi cetakan rumusnya. |
| 102-103 | ringkas.setName(...) lalu addMapLayer | Beri nama Ringkasan_KPH dan tampilkan. |
Blok 5: analisis raster (baris 105-122). Pola dari Skrip 21.3.
| Baris | Tulisan | Artinya |
|---|---|---|
| 106-107 | ndvi_jalur = jalankan("native:rastercalc", ...) | Hitung NDVI ke berkas NDVI_KPH.tif. Rumusnya (NIR - Merah) / (NIR + Merah). Penulisan "Citra_KPH@4" berarti band nomor 4 dari layer Citra_KPH, yaitu inframerah dekat. Band 1 adalah merah. |
| 108-109 | chm_jalur = jalankan("native:rastercalc", ...) | Hitung tinggi tajuk (CHM) sebagai DSM dikurangi DTM, ke berkas CHM.tif. |
| 110-113 | muat_raster(...) dan addMapLayer | Buka kedua hasil dan tampilkan. Alat raster mengembalikan jalur teks, bukan layer. |
| 116-119 | def rata(raster, awalan): | Hitung rata-rata raster di dalam tiap KPH dengan native:zonalstatisticsfb. STATISTICS=[2] berarti rata-rata (mean). Kamus hasilnya memasangkan nama KPH dengan rata-ratanya. Kolom hasil bernama awalan ditambah mean. |
| 122 | ndvi_rata, chm_rata = rata(ndvi, "n_"), rata(chm, "c_") | Dua pemanggilan dalam satu baris, hasilnya masuk ke dua kotak. |
Blok 6: tabel ringkasan (baris 124-137).
| Baris | Tulisan | Artinya |
|---|---|---|
| 125-130 | judul = [...] dan baris = [] lalu perulangan | Susun satu baris per KPH: nama, luas, sempadan, titik panas, titik survei, NDVI, dan tinggi tajuk, yang dibulatkan. |
| 131-134 | with open(... "ringkasan_kph.csv" ...) | Tulis ke CSV: baris judul, lalu semua baris. |
| 135-137 | print("%-10s ..." % tuple(judul)) | Cetak tabel di layar. tuple(...) mengubah daftar menjadi tuple, karena operator % memintanya. Format %7.2f memberi tujuh kolom dengan dua desimal. |
Blok 7: kemas dan simpan (baris 139-149).
| Baris | Tulisan | Artinya |
|---|---|---|
| 141-143 | processing.run("native:package", {...}) | Tulis tiga layer sementara ke satu berkas GeoPackage. OVERWRITE: True menimpa berkas lama. |
| 144-147 | for lyr in (survei, sempadan, ringkas): | Untuk tiap layer sementara: hapus dari proyek, lalu tambahkan layer yang sama dari GeoPackage. Alamatnya dibentuk dari jalur berkas, tanda garis tegak, layername=, dan nama tabel. |
| 148 | proyek.write(HASIL + "/Proyek_Terpadu.qgz") | Simpan proyek. Hasilnya True bila berhasil. |
| 149 | print("Isi proyek:", sorted(...)) | Cetak daftar nama layer di proyek, urut abjad. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Kiriman: 4 diterima, 2 ditolak; titik gabungan = 18
NAMA_KPH LUAS_HA SEMPADAN_HA HOTSPOT TITIK_SURVEI NDVI_RATA CHM_RATA_M
KPH Alpha 4.00 1.18 2 6 0.604 2.60
KPH Beta 4.00 1.39 5 4 0.431 2.69
KPH Gamma 8.00 1.50 5 7 0.329 2.56
Proyek tersimpan? True
Isi proyek: ['Batas_KPH', 'CHM', 'Citra_KPH', 'Hotspot_Nasional', 'NDVI_KPH', 'Ringkasan_KPH', 'Sempadan_30m', 'Sungai', 'Titik_Survei_Gabungan']Satu tabel ini menjawab semua pertanyaan Kepala Seksi. KPH Gamma terluas (8 ha). KPH Beta punya sempadan terbesar terhadap luasnya, dan titik panas Beta dan Gamma masing-masing 5. NDVI turun dari Alpha (0,604) ke Gamma (0,329). Nilai itu sama dengan keterangan di README.txt paket data.
Ada satu angka yang layak dicermati. TITIK_SURVEI berjumlah 6 + 4 + 7 = 17, padahal titik gabungan = 18. Selisih satu itu adalah titik yang jatuh di luar semua KPH. Ia sama dengan titik curiga b_05.jpg di Skrip 22.2. Jadi agregasi per KPH tidak menghitungnya, dan ini benar.
Bagian yang boleh diubah.
- Baris 10-11, jalur. Sesuaikan dengan komputer Anda.
- Baris 14, CRS. Ganti sesuai wilayah Anda. Akibatnya, semua kiriman diubah ke CRS itu.
- Baris 65, kolom wajib.
("Kondisi", "Tinggi_Phn", "Foto")boleh diganti kolom lain. - Baris 88-89, jarak 30 dan nama
Sempadan_30m. Ubah keduanya bersamaan. Akibatnya, kolomSEMPADAN_HAberubah. - Baris 99-101, kolom ringkasan. Tambah kolom baru dengan satu panggilan
hitung_kolomlagi. - Baris 106-109, rumus raster. Ganti nomor band bila citra Anda berbeda.
Jebakan.
- Proyek Anda dikosongkan. Baris 17 menghapus semua layer dari proyek yang terbuka, dan baris 18-19 mengubah CRS serta judulnya. Baris 148 menyimpan proyek ke berkas baru. Jalankan di proyek kosong [CEK: perilaku jendela QGIS setelah penyimpanan tidak dapat diuji dari sini].
fidkembar. Pada uji, baris 78 digantisurvei = gabung. Skrip berjalan sampai tabel tercetak, lalu berhenti di baris 142 denganQgsProcessingException: Error obtained while packaging one or more layers.Di layar juga munculERROR 1: failed to execute insert : UNIQUE constraint failed: Titik_Survei_Gabungan.fid. Itu sebabnya kolomfiddibuang.- Nama layer di rumus harus sama. Pada uji,
"Hotspot_Nasional"di baris 100 diganti"Hotspot". Skrip berhenti di baris 100 denganCannot find layer with name or ID 'Hotspot'. Berbeda dengan Skrip 22.4, galat ini menghentikan skrip. - Layer sementara hilang. Layer hasil alat bersifat sementara. Itu sebabnya baris 139-147 memindahkannya ke GeoPackage. Bila bagian itu dihapus, proyek yang tersimpan menunjuk layer yang tidak ada lagi.
- Dua cara menghitung luas. Kolom
LUAS_HAmemakai$area(mengikuti ellipsoid proyek), sedangkanSEMPADAN_HAmemakaigeometry().area()(bidang datar). Bedanya sangat kecil, dan dibahas pada Skrip 22.4.
Latihan variasi.
- Ubah lebar sempadan menjadi 50 meter.
- Cetak tambahan: sempadan sebagai persen dari luas KPH.
Contoh jawaban nomor 1 (sudah diuji): ubah baris 88 dan 89 menjadi
sempadan = jalankan("native:buffer", INPUT=sungai, DISTANCE=50, DISSOLVE=True, OUTPUT=SEMENTARA)
sempadan.setName("Sempadan_50m")Kolom SEMPADAN_HA berubah menjadi 1,69, 2,32, dan 2,54. Angka ini sesuai dengan hasil model di Skrip 23.2 untuk 50 meter (jumlah 6,55 ha).
Contoh jawaban nomor 2 (sudah diuji): tambahkan di akhir skrip.
for b in baris:
print(b[0], "sempadan = %.1f persen dari luas" % (100 * b[2] / b[1]))Layar menambah KPH Alpha sempadan = 29.5 persen dari luas, KPH Beta sempadan = 34.8 persen dari luas, dan KPH Gamma sempadan = 18.8 persen dari luas.
Skrip 23.4: Ringkasan sebagai tugas latar belakang #
Skrip 23.3 membuat QGIS terasa macet selama bekerja. Skrip ini mengerjakan ringkasan yang sama sebagai tugas latar belakang, dengan pola QgsTask dari Bab 21 (Skrip 21.6). Karena itu hanya bagian yang baru dijelaskan.
Tujuan. Menghitung ringkasan per KPH di latar belakang, melaporkan kemajuan, lalu menampilkan hasilnya saat tugas selesai.
Bahan. Paket paket-i4: batas, sungai, titik panas, citra, DSM, dan DTM. Berkas b7_02_tugas_ringkasan.py (di dalamnya tertulis "Skrip 7.2"). Ganti DATA di baris 10 dan HASIL di baris 11. Skrip ini paling pas dijalankan di Editor Skrip QGIS yang terbuka, karena tugas latar belakang butuh QGIS yang hidup. Hasilnya: NDVI_KPH.tif, CHM.tif, dan ringkasan_latar.csv.

Skrip utuh:
# [SKRIP 7.2: Ringkasan per KPH sebagai tugas latar belakang (QgsTask)]
# Penulis: Badar Mubarok Yogaswara
# Pola: pekerjaan berat di fungsi tugas (latar belakang); proyek dan tabel di fungsi selesai (utama).
import csv
import os
import processing
from qgis.core import (QgsApplication, QgsTask, QgsMessageLog, Qgis, QgsProject,
QgsVectorLayer, QgsRasterLayer)
DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
os.makedirs(HASIL, exist_ok=True)
SEMENTARA = "TEMPORARY_OUTPUT"
def jalankan(alat, **parameter):
return processing.run(alat, parameter)["OUTPUT"]
def hitung_ringkasan(task, data, hasil):
"""Latar belakang. Hanya membaca berkas dan menjalankan alat; tidak menyentuh QgsProject."""
batas = QgsVectorLayer(data + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
sungai = QgsVectorLayer(data + "/Sungai.gpkg", "Sungai", "ogr")
hotspot = QgsVectorLayer(data + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")
citra = QgsRasterLayer(data + "/Citra_KPH.tif", "Citra_KPH", "gdal")
dsm = QgsRasterLayer(data + "/DSM_Drone.tif", "DSM", "gdal")
dtm = QgsRasterLayer(data + "/DTM_Drone.tif", "DTM", "gdal")
for lyr in (batas, sungai, hotspot, citra, dsm, dtm):
if not lyr.isValid():
raise ValueError("Layer tidak terbaca: " + lyr.name())
# tahap 1 (0-25%): sempadan sungai
sem = jalankan("native:buffer", INPUT=sungai, DISTANCE=30, DISSOLVE=True, OUTPUT=SEMENTARA)
iris = jalankan("native:intersection", INPUT=batas, OVERLAY=sem, INPUT_FIELDS=["NAMA_KPH"],
OVERLAY_FIELDS=[], OUTPUT=SEMENTARA)
sem_ha = {}
for f in iris.getFeatures():
sem_ha[f["NAMA_KPH"]] = sem_ha.get(f["NAMA_KPH"], 0) + f.geometry().area() / 10000
task.setProgress(25)
if task.isCanceled():
return None
# tahap 2 (25-50%): titik panas
hit = jalankan("native:countpointsinpolygon", POLYGONS=batas, POINTS=hotspot, FIELD="HOTSPOT",
OUTPUT=SEMENTARA)
titik = {f["NAMA_KPH"]: f["HOTSPOT"] for f in hit.getFeatures()}
task.setProgress(50)
if task.isCanceled():
return None
# tahap 3 (50-100%): raster
ndvi = jalankan("native:rastercalc", LAYERS=[citra], OUTPUT=hasil + "/NDVI_KPH.tif",
EXPRESSION='("Citra_KPH@4" - "Citra_KPH@1") / ("Citra_KPH@4" + "Citra_KPH@1")')
chm = jalankan("native:rastercalc", LAYERS=[dsm, dtm], OUTPUT=hasil + "/CHM.tif",
EXPRESSION='"DSM@1" - "DTM@1"')
task.setProgress(75)
def rata(jalur, awalan):
raster = QgsRasterLayer(jalur, "r", "gdal")
z = jalankan("native:zonalstatisticsfb", INPUT=batas, INPUT_RASTER=raster, RASTER_BAND=1,
COLUMN_PREFIX=awalan, STATISTICS=[2], OUTPUT=SEMENTARA)
return {f["NAMA_KPH"]: f[awalan + "mean"] for f in z.getFeatures()}
n, c = rata(ndvi, "n_"), rata(chm, "c_")
task.setProgress(100)
hasil_baris = []
for f in batas.getFeatures():
k = f["NAMA_KPH"]
hasil_baris.append([k, round(f.geometry().area() / 10000, 2), round(sem_ha.get(k, 0), 2),
titik[k], round(n[k], 3), round(c[k], 2)])
return hasil_baris
def selesai(exception, baris=None):
"""Program utama. Aman menyentuh proyek, tabel, dan tampilan."""
if exception is not None:
QgsMessageLog.logMessage("Ringkasan gagal: %s" % exception, "Ringkasan KPH", Qgis.Critical)
return
if baris is None:
QgsMessageLog.logMessage("Ringkasan dibatalkan", "Ringkasan KPH", Qgis.Warning)
return
with open(HASIL + "/ringkasan_latar.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["NAMA_KPH", "LUAS_HA", "SEMPADAN_HA", "HOTSPOT", "NDVI_RATA", "CHM_RATA_M"])
w.writerows(baris)
for nama in ("NDVI_KPH", "CHM"):
QgsProject.instance().addMapLayer(QgsRasterLayer(HASIL + "/" + nama + ".tif", nama, "gdal"))
for b in baris:
print("%-10s luas %5.2f ha | sempadan %4.2f ha | titik panas %d | NDVI %.3f | CHM %.2f m"
% tuple(b))
QgsMessageLog.logMessage("Ringkasan selesai: %d KPH" % len(baris), "Ringkasan KPH", Qgis.Success)
tugas = QgsTask.fromFunction("Ringkasan per KPH", hitung_ringkasan, on_finished=selesai,
data=DATA, hasil=HASIL)
QgsApplication.taskManager().addTask(tugas)
print("Tugas berjalan di latar belakang; QGIS tetap bisa dipakai.")Penjelasan bagian yang baru. Nomor baris mengikuti berkas di atas.
| Baris | Tulisan | Artinya |
|---|---|---|
| 4-13 | impor, DATA, HASIL, SEMENTARA | Sama seperti Skrip 23.3, ditambah QgsApplication, QgsTask, dan QgsMessageLog untuk tugas dan catatan. |
| 16-17 | def jalankan(alat, **parameter): | Pembantu yang sama seperti di Skrip 23.3. |
| 20 | def hitung_ringkasan(task, data, hasil): | Fungsi tugas. Isian pertama, task, dikirim QGIS otomatis. Dua isian lainnya berasal dari baris 94-95. |
| 22-27 | enam pemanggilan QgsVectorLayer dan QgsRasterLayer | Layer dibuka di dalam fungsi tugas, bukan diambil dari proyek. Fungsi tugas tidak boleh menyentuh proyek (komentar di baris 21). |
| 28-30 | for lyr in (...): lalu if not lyr.isValid(): dan raise ValueError(...) | Pengaman. Tiap layer dicek satu per satu. Bila ada yang tidak terbaca, tugas berhenti dengan pesan Layer tidak terbaca: dan nama layernya. isValid() dan raise ValueError dibahas di Bab 10. |
| 32-41 | tahap 1: sempadan | Buffer dan irisan seperti Skrip 23.3. task.setProgress(25) melaporkan kemajuan 25 persen. if task.isCanceled(): return None berhenti bila pengguna membatalkan tugas. |
| 43-49 | tahap 2: titik panas | Alat native:countpointsinpolygon menambah kolom HOTSPOT, yaitu jumlah titik di tiap KPH. Hasilnya dijadikan kamus nama dan jumlah. Lalu kemajuan 50 persen dan pemeriksaan pembatalan lagi. |
| 51-56 | tahap 3a: NDVI dan CHM | Dua native:rastercalc seperti Skrip 23.3. Kemajuan 75 persen. |
| 58-62 | def rata(jalur, awalan): | Fungsi di dalam fungsi, karena hanya dipakai di sini. Ia membuka raster hasil dan menghitung rata-rata per KPH. |
| 64-65 | n, c = rata(...), rata(...) lalu task.setProgress(100) | Hitung dua rata-rata, lalu lapor kemajuan penuh. |
| 66-71 | hasil_baris = [] lalu return hasil_baris | Susun satu daftar berisi satu baris per KPH, lalu kembalikan. Nilai kembalian ini yang diterima fungsi selesai. Luas KPH dihitung langsung dari geometri (f.geometry().area()). |
| 74 | def selesai(exception, baris=None): | Fungsi selesai. exception berisi galat bila tugas gagal. baris berisi daftar dari return. |
| 76-81 | if exception is not None: lalu if baris is None: | Tiga jalur: gagal (catat Qgis.Critical), dibatalkan (catat Qgis.Warning), atau sukses. |
| 82-85 | with open(... "ringkasan_latar.csv" ...) | Tulis CSV. Berbeda dari Skrip 23.3, tidak ada kolom TITIK_SURVEI, karena versi ini tidak menggabung kiriman lapangan. |
| 86-87 | for nama in ("NDVI_KPH", "CHM"): | Tambahkan dua raster hasil ke proyek. Ini aman di sini karena fungsi selesai berjalan di program utama. |
| 88-91 | for b in baris: print(...) | Cetak satu baris per KPH, lalu catat Qgis.Success di log. |
| 94-95 | tugas = QgsTask.fromFunction("Ringkasan per KPH", hitung_ringkasan, on_finished=selesai, data=DATA, hasil=HASIL) | Bungkus fungsi tugas menjadi tugas. Dua isian bernama di ujung diteruskan ke hitung_ringkasan. |
| 96-97 | addTask(tugas) lalu print(...) | Serahkan tugas ke manajer tugas QGIS. Pesan di baris 97 muncul sebelum hasil, karena skrip tidak menunggu. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
Tugas berjalan di latar belakang; QGIS tetap bisa dipakai.
KPH Alpha luas 4.00 ha | sempadan 1.18 ha | titik panas 2 | NDVI 0.604 | CHM 2.60 m
KPH Beta luas 4.00 ha | sempadan 1.39 ha | titik panas 5 | NDVI 0.431 | CHM 2.69 m
KPH Gamma luas 8.00 ha | sempadan 1.50 ha | titik panas 5 | NDVI 0.329 | CHM 2.56 mBaris pertama muncul langsung. Tiga baris sisanya muncul belakangan, setelah tugas selesai. Angkanya sama dengan Skrip 23.3: sempadan 1,18, 1,39, dan 1,50 ha; titik panas 2, 5, dan 5; NDVI 0,604, 0,431, dan 0,329; tinggi tajuk 2,60, 2,69, dan 2,56 meter. Dua cara berbeda, jawaban sama. Itulah tanda yang baik. Kemajuan tugas tampil di bilah status QGIS dan pesan lognya di panel Log Messages dengan tag Ringkasan KPH [CEK: tampilan visualnya tidak dapat diuji tanpa antarmuka QGIS].
Bagian yang boleh diubah.
- Baris 10-11, jalur. Sesuaikan dengan komputer Anda.
- Baris 33, jarak 30. Ubah lebar sempadan. Akibatnya, kolom
sempadanberubah. - Baris 39, 47, 56, dan 65, angka kemajuan. Atur pembagian bilah kemajuan sesuka Anda.
- Baris 77, 80, dan 91, tag log
"Ringkasan KPH". Ganti dengan nama yang Anda suka. - Baris 94, judul tugas. Judul ini tampil di daftar tugas QGIS.
Jebakan.
- Galat di latar belakang tidak terlihat di layar. Pada uji,
data=DATAdi baris 95 diubah menjadidata=DATA + "/salah". Layar hanya menampilkanTugas berjalan di latar belakang; QGIS tetap bisa dipakai.dan tidak ada pesan lain. Galatnya hanya masuk ke log, dengan teksRingkasan gagal: Layer tidak terbaca: Batas_KPH, sebab pengaman di baris 28-30 menangkap layer yang gagal dibuka (diverifikasi dengan salinan skrip yang mencetak teks log). Bila pengaman itu dihapus, teksnyaRingkasan gagal: Could not create memory layer. Bila hasil tidak muncul, buka panel Log Messages. - Baris pengaman (28-30) jangan dihapus. Layer yang gagal dibuka tidak menimbulkan galat sendiri. Tanpa pengaman, tugas tidak berhenti dan mencetak tinggi tajuk yang ngawur: pada uji tercetak
CHM 0.00 matauCHM inf m, dan nilainya bisa berbeda tiap kali dijalankan. Pada uji, nama berkas DSM di baris 26 digantiDSM_Hilang.tif. Dengan pengaman, tugas berhenti, tidak ada CSV, dan log memuatRingkasan gagal: Layer tidak terbaca: DSM. Seperti galat lain di latar belakang, pesannya hanya terlihat di panel Log Messages. Saat menambah layer baru, tambahkan juga ke daftar di baris 28. - Jangan menyentuh proyek di fungsi tugas. Menambah layer dari latar belakang bisa membuat QGIS macet atau tertutup [CEK: tidak diuji di antarmuka QGIS].
- Dijalankan di luar jendela QGIS. Bila skrip dijalankan tanpa antarmuka, tugas harus ditunggu dengan perulangan, seperti dibahas pada Skrip 21.6. Pada uji, perulangan itu dipakai.
Latihan variasi.
- Ubah sempadan menjadi 50 meter.
- Bandingkan hasilnya dengan latihan serupa di Skrip 23.3.
Contoh jawaban (sudah diuji): ubah DISTANCE=30 di baris 33 menjadi DISTANCE=50. Kolom sempadan menjadi 1,69, 2,32, dan 2,54 ha. Angka ini sama dengan hasil Skrip 23.3 yang diubah ke 50 meter, sehingga dua jalur berbeda memberi jawaban yang sama.
Skrip 23.5: Ceklis masuk M1 #
Skrip ini unik. Ia bukan pekerjaan analisis, melainkan ujian untuk Anda: sepuluh pemeriksaan otomatis yang mencetak LULUS atau GAGAL, lalu skor akhir. Bagian terpentingnya adalah cara tiap pemeriksaan ditulis, karena pola itu bisa Anda pakai untuk menguji skrip sendiri.
Tujuan. Menguji sepuluh keterampilan yang dipakai kelas M1, lalu menyebut bab mana yang perlu diulang.
Bahan. Paket paket-i4: Batas_KPH.gpkg, Hotspot_Nasional.gpkg, Citra_KPH.tif, dan Kiriman_Lapangan/tim_e.gpkg. Berkas b7_03_ceklis_masuk_m1.py (di dalamnya tertulis "Skrip 7.3"). Ganti DATA di baris 13 dan HASIL di baris 14. Keluarannya uji_kesiapan.qgz. Jalankan di proyek kosong. Baris 18 dan 122 mengosongkan proyek terbuka. Skor akhir muncul belakangan, karena pemeriksaan ke-10 berjalan di latar belakang.

Skrip utuh:
# [SKRIP 7.3: Uji kesiapan otomatis: sepuluh keterampilan yang dipakai Masterclass Seri 1 (M1)]
# Penulis: Badar Mubarok Yogaswara
# Tiap pemeriksaan mencetak LULUS atau GAGAL beserta alasannya.
# Skor akhir muncul setelah tugas latar belakang (pemeriksaan 10) selesai.
import os
import re
import time
import processing
from qgis.core import (QgsApplication, QgsTask, QgsProject, QgsVectorLayer, QgsRasterLayer,
QgsExpression, QgsExpressionContext, QgsExpressionContextUtils,
QgsProcessingException)
DATA = "D:/Latihan/paket-i4"
HASIL = "D:/Latihan/hasil_i4"
os.makedirs(HASIL, exist_ok=True)
proyek = QgsProject.instance()
proyek.removeAllMapLayers()
catatan = [] # daftar (nama, lulus?, alasan)
NDVI = '("Citra_KPH@4" - "Citra_KPH@1") / ("Citra_KPH@4" + "Citra_KPH@1")'
CACAH = ("aggregate('Hotspot_Nasional', 'count', \"ID_Titik\", "
"intersects($geometry, geometry(@parent)))")
def periksa(nama, fungsi):
try:
alasan = fungsi()
catatan.append((nama, True, alasan or ""))
except Exception as e: # pemeriksaan yang gagal dicatat; skrip tidak berhenti
catatan.append((nama, False, "%s: %s" % (type(e).__name__, e)))
n, ok, alasan = catatan[-1]
print("%-46s %s %s" % (n, "LULUS" if ok else "GAGAL", alasan))
def tegaskan(syarat, pesan):
if not syarat:
raise AssertionError(pesan)
batas = QgsVectorLayer(DATA + "/Batas_KPH.gpkg", "Batas_KPH", "ogr")
hotspot = QgsVectorLayer(DATA + "/Hotspot_Nasional.gpkg", "Hotspot_Nasional", "ogr")
citra = QgsRasterLayer(DATA + "/Citra_KPH.tif", "Citra_KPH", "gdal")
def konteks(fitur):
c = QgsExpressionContext()
c.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(batas))
c.setFeature(fitur)
return c
def k1():
for lyr in (batas, hotspot, citra):
tegaskan(lyr.isValid(), lyr.name() + " tidak sah")
proyek.addMapLayer(lyr)
tegaskan(len(proyek.mapLayers()) == 3, "jumlah layer tidak 3")
def k2():
tegaskan(proyek.mapLayersByName("Batas_KPH")[0].featureCount() == 3, "Batas_KPH bukan 3 fitur")
r = proyek.mapLayersByName("Citra_KPH")[0]
tegaskan(r.bandCount() == 4 and r.rasterUnitsPerPixelX() == 2.0, "raster tidak sesuai")
tegaskan(proyek.mapLayersByName("Tidak_Ada") == [], "nama tak ada harus memberi daftar kosong")
return "band=%d, piksel=%.0f m" % (r.bandCount(), r.rasterUnitsPerPixelX())
def k3():
v = processing.run("native:buffer", {"INPUT": batas, "DISTANCE": 10,
"OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
tegaskan(isinstance(v, QgsVectorLayer) and v.featureCount() == 3, "bukan layer vektor")
r = processing.run("native:rastercalc", {"EXPRESSION": '"Citra_KPH@4" - "Citra_KPH@1"',
"LAYERS": [citra], "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
tegaskan(isinstance(r, str) and os.path.exists(r), "keluaran raster bukan jalur berkas")
return "vektor -> layer, raster -> jalur teks"
def k4():
jalur = processing.run("native:rastercalc", {"EXPRESSION": NDVI, "LAYERS": [citra],
"OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
st = processing.run("native:rasterlayerstatistics",
{"INPUT": QgsRasterLayer(jalur, "ndvi", "gdal"), "BAND": 1})
tegaskan(-1 <= st["MIN"] <= st["MAX"] <= 1, "NDVI di luar -1..1")
return "NDVI %.2f sampai %.2f" % (st["MIN"], st["MAX"])
def k5():
e = QgsExpression(CACAH)
hasil = {f["NAMA_KPH"]: e.evaluate(konteks(f)) for f in batas.getFeatures()}
benar = {f["NAMA_KPH"]: sum(1 for h in hotspot.getFeatures()
if h.geometry().intersects(f.geometry())) for f in batas.getFeatures()}
tegaskan(len(hasil) == 3, "agregasi tidak menghasilkan 3 nilai: %s" % hasil)
tegaskan(hasil == benar, "agregasi %s beda dari hitungan manual %s" % (hasil, benar))
return str(hasil)
def k6():
e = QgsExpression("'Titik api: ' || " + CACAH + " || ' titik'")
beta = [x for x in batas.getFeatures() if x["NAMA_KPH"] == "KPH Beta"][0]
teks = e.evaluate(konteks(beta))
tegaskan(not e.hasEvalError() and teks == "Titik api: 5 titik", "teks label salah: %s" % teks)
return teks
def k7():
try:
processing.run("native:buffer", {"INPUT": DATA + "/tidak_ada.gpkg", "DISTANCE": 5,
"OUTPUT": "TEMPORARY_OUTPUT"})
except QgsProcessingException:
return "galat alat tertangkap"
raise AssertionError("seharusnya gagal")
def k8():
lyr = QgsVectorLayer(DATA + "/Kiriman_Lapangan/tim_e.gpkg", "rusak", "ogr")
tegaskan(not lyr.isValid(), "berkas rusak dianggap sah")
return "isValid() = False untuk berkas rusak"
def k9():
berkas = HASIL + "/uji_kesiapan.qgz"
tegaskan(proyek.write(berkas), "gagal menulis proyek")
proyek.clear()
tegaskan(len(proyek.mapLayers()) == 0, "proyek belum kosong")
tegaskan(proyek.read(berkas) and len(proyek.mapLayers()) == 3, "proyek tidak terbaca utuh")
return "tulis, kosongkan, baca ulang: 3 layer"
daftar = [("1. Muat vektor dan raster [Bab 1]", k1), ("2. Ambil layer dengan nama [Bab 1]", k2),
("3. processing.run sementara [Bab 2]", k3), ("4. Raster calculator NDVI [Bab 2]", k4),
("5. Agregasi dan @parent [Bab 5]", k5), ("6. Teks label dari ekspresi [Bab 5]", k6),
("7. Menangkap galat alat [Bab 4]", k7), ("8. Mendeteksi berkas rusak [Bab 4]", k8),
("9. Simpan dan buka proyek [Bab 1]", k9)]
for nama, fungsi in daftar:
periksa(nama, fungsi)
# 10. Tugas latar belakang. Hasilnya tiba belakangan, jadi skor akhir dicetak di selesai().
def kerja(task):
total = 0
for i in range(1, 6):
if task.isCanceled():
return None
time.sleep(0.05)
total += i
task.setProgress(i * 20)
return total
def selesai(exception, nilai=None):
ok = exception is None and nilai == 15
catatan.append(("10. QgsTask sederhana [Bab 3]", ok, "hasil tugas = %s" % nilai))
print("%-46s %s hasil tugas = %s" % (catatan[-1][0], "LULUS" if ok else "GAGAL", nilai))
lulus = sum(1 for _, o, _ in catatan if o)
gagal = sorted({re.search(r"\[(Bab \d)\]", n).group(1) for n, o, _ in catatan if not o})
akhir = "Siap masuk M1." if not gagal else "Ulangi: " + ", ".join(gagal) + "."
print("SKOR: %d dari %d. %s" % (lulus, len(catatan), akhir))
tugas_uji = QgsTask.fromFunction("Uji tugas latar belakang", kerja, on_finished=selesai)
QgsApplication.taskManager().addTask(tugas_uji)Penjelasan baris demi baris. Skrip ini 160 baris, jadi dibahas dalam lima blok. Nomor baris mengikuti berkas di atas.
Blok 1: bahan dan dua pembantu (baris 1-49).
| Baris | Tulisan | Artinya |
|---|---|---|
| 5-11 | import ... dan from qgis.core import (...) | Perkakas yang sudah dikenal, ditambah re untuk mencari pola teks dan time untuk jeda. |
| 17-19 | proyek = ..., removeAllMapLayers(), catatan = [] | Ambil proyek, kosongkan, dan siapkan daftar catatan. Tiap catatan berisi nama, lulus atau tidak, dan alasan. |
| 20-22 | NDVI dan CACAH | Dua rumus yang dipakai pemeriksaan, ditulis sekali di atas. |
| 25-32 | def periksa(nama, fungsi): | Jantung ceklis. Fungsi pemeriksaan dijalankan di dalam try. Bila berhasil, catat lulus dengan alasan yang dikembalikan fungsi itu. Bila galat apa pun, catat gagal dengan type(e).__name__ (jenis galat) dan pesannya. Baris 31-32 mencetak hasilnya. |
| 35-37 | def tegaskan(syarat, pesan): | Bila syarat salah, raise AssertionError(pesan). Pemeriksaan lain cukup memanggil fungsi ini. |
| 40-42 | tiga layer | Buka layer batas, titik panas, dan citra. |
| 45-49 | def konteks(fitur): | Pembantu konteks ekspresi, sama dengan Skrip 22.4. |
Perhatikan bahwa periksa memakai except Exception, seperti Skrip 22.2. Di sini itu memang disengaja: ceklis harus tetap berjalan sampai akhir, apa pun yang gagal.
Blok 2: sembilan pemeriksaan, k1 sampai k9 (baris 52-125). Tiap fungsi menguji satu keterampilan. Fungsi yang tidak memanggil raise dianggap lulus.
| Pemeriksaan | Baris | Yang diuji | Asal materi |
|---|---|---|---|
| 1 | 52-56 | Tiga layer terbuka (isValid()), masuk proyek, jumlahnya 3 | Bab 14 |
| 2 | 59-64 | mapLayersByName memberi layer yang benar. Batas berisi 3 fitur, citra 4 band dan piksel 2 meter. Nama yang tidak ada memberi daftar kosong | Bab 14 |
| 3 | 67-74 | Hasil native:buffer berupa layer vektor, hasil native:rastercalc berupa jalur teks berkas | Bab 21 |
| 4 | 77-83 | NDVI dihitung dengan rastercalc, lalu nilainya harus antara -1 dan 1 (perbandingan berantai) | Bab 21 |
| 5 | 86-93 | Agregasi @parent menghasilkan tiga angka, dan sama dengan hitungan manual memakai intersects | Bab 22 |
| 6 | 96-101 | Teks label Titik api: 5 titik untuk KPH Beta | Bab 22 |
| 7 | 104-110 | Alat dengan masukan yang tidak ada harus gagal. Bila tidak gagal, raise AssertionError("seharusnya gagal") | Bab 22 |
| 8 | 113-116 | Berkas rusak tim_e.gpkg memberi isValid() bernilai False | Bab 22 |
| 9 | 119-125 | Tulis proyek, kosongkan (proyek.clear()), baca ulang, dan tiga layer kembali | Bab 14 |
Pada pemeriksaan 3, isinstance(v, QgsVectorLayer) bertanya: "apakah v berjenis layer vektor?" Jawabannya True atau False.
Pemeriksaan 7 menarik: ia menguji bahwa galat memang terjadi. Kalimat terakhirnya, raise AssertionError, hanya tercapai bila alat tidak gagal, dan itu justru kegagalan ujiannya.
Blok 3: daftar fungsi (baris 128-134).
| Baris | Tulisan | Artinya |
|---|---|---|
| 128-132 | daftar = [("1. ...", k1), ...] | Daftar pasangan: nama pemeriksaan dan fungsinya. Fungsi boleh dipakai sebagai nilai. Tulis k1 tanpa kurung, karena yang disimpan adalah fungsinya, bukan hasilnya. |
| 133-134 | for nama, fungsi in daftar: periksa(nama, fungsi) | Jalankan semuanya dengan satu perulangan. Menambah pemeriksaan baru cukup menambah satu pasangan. |
Blok 4: pemeriksaan 10, tugas latar belakang (baris 137-146).
| Baris | Tulisan | Artinya |
|---|---|---|
| 138-146 | def kerja(task): | Fungsi tugas sederhana: menjumlahkan 1 sampai 5 dengan jeda singkat (time.sleep(0.05)), lalu mengembalikan 15. |
Blok 5: skor akhir (baris 149-160).
| Baris | Tulisan | Artinya |
|---|---|---|
| 150-152 | def selesai(exception, nilai=None): | Pemeriksaan ke-10 lulus bila tidak ada galat dan hasilnya 15. Catat dan cetak seperti pemeriksaan lain. |
| 153 | lulus = sum(1 for _, o, _ in catatan if o) | Hitung catatan yang lulus. |
| 154 | gagal = sorted({re.search(r"\[(Bab \d)\]", n).group(1) for ...}) | Dari nama pemeriksaan yang gagal, ambil bagian [Bab n] dengan pola teks, kumpulkan tanpa kembar (himpunan), lalu urutkan. |
| 155-156 | akhir = ... lalu print("SKOR: ...") | Bila tidak ada yang gagal, cetak "Siap masuk M1." Bila ada, cetak bab yang perlu diulang. |
| 159-160 | QgsTask.fromFunction(...) lalu addTask | Buat dan kirim tugas, seperti Skrip 23.4. |
Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):
1. Muat vektor dan raster [Bab 1] LULUS
2. Ambil layer dengan nama [Bab 1] LULUS band=4, piksel=2 m
3. processing.run sementara [Bab 2] LULUS vektor -> layer, raster -> jalur teks
4. Raster calculator NDVI [Bab 2] LULUS NDVI -0.02 sampai 0.84
5. Agregasi dan @parent [Bab 5] LULUS {'KPH Alpha': 2, 'KPH Beta': 5, 'KPH Gamma': 5}
6. Teks label dari ekspresi [Bab 5] LULUS Titik api: 5 titik
7. Menangkap galat alat [Bab 4] LULUS galat alat tertangkap
8. Mendeteksi berkas rusak [Bab 4] LULUS isValid() = False untuk berkas rusak
9. Simpan dan buka proyek [Bab 1] LULUS tulis, kosongkan, baca ulang: 3 layer
10. QgsTask sederhana [Bab 3] LULUS hasil tugas = 15
SKOR: 10 dari 10. Siap masuk M1.Sepuluh lulus, skor 10 dari 10. Baris 10. dan SKOR muncul paling akhir, setelah tugas latar belakang selesai.
Label [Bab n] menunjuk bab Buku I4, bukan bab buku ini. Skrip ini ditulis untuk buku seri. Jadi [Bab 1] berarti Bab 14 buku ini, [Bab 2] dan [Bab 3] berarti Bab 21, serta [Bab 4] dan [Bab 5] berarti Bab 22. Bila skor Anda kurang, baca pesan "Ulangi: Bab n" dengan terjemahan itu.
Uji negatif: membuktikan ceklis bisa berkata GAGAL #
Ceklis yang selalu LULUS tidak berguna. Karena itu penulisnya menyertakan satu berkas hasil uji negatif, hasil_uji/b7_03_ceklis_uji_negatif.txt. Keterangannya menyebut bahwa nama berkas Batas_KPH.gpkg pada skrip sengaja diubah menjadi Batas_KPX.gpkg. Skrip uji negatif itu sendiri tidak ada di folder seri. Yang tersedia hanya hasilnya.
Agar jujur, bagian ini tidak mengandalkan berkas hasil itu saja. Untuk bab ini, uji negatif dibuat ulang: satu salinan skrip diubah di baris 40 saja, yaitu "/Batas_KPH.gpkg" menjadi "/Batas_KPX.gpkg", lalu dijalankan. Hasilnya sama persis, baris demi baris, dengan berkas hasil uji milik penulis. Apakah penulis mengubah tepat baris itu, tidak bisa dipastikan dari berkasnya. Tetapi satu perubahan itu menghasilkan keluaran yang sama, jadi penjelasannya masuk akal.
Hasil uji negatif:
1. Muat vektor dan raster [Bab 1] GAGAL AssertionError: Batas_KPH tidak sah
2. Ambil layer dengan nama [Bab 1] GAGAL IndexError: list index out of range
3. processing.run sementara [Bab 2] GAGAL AssertionError: bukan layer vektor
4. Raster calculator NDVI [Bab 2] LULUS NDVI -0.02 sampai 0.84
5. Agregasi dan @parent [Bab 5] GAGAL AssertionError: agregasi tidak menghasilkan 3 nilai: {}
6. Teks label dari ekspresi [Bab 5] GAGAL IndexError: list index out of range
7. Menangkap galat alat [Bab 4] LULUS galat alat tertangkap
8. Mendeteksi berkas rusak [Bab 4] LULUS isValid() = False untuk berkas rusak
9. Simpan dan buka proyek [Bab 1] GAGAL AssertionError: proyek tidak terbaca utuh
10. QgsTask sederhana [Bab 3] LULUS hasil tugas = 15
SKOR: 4 dari 10. Ulangi: Bab 1, Bab 2, Bab 5.Cara membacanya.
- Baca GAGAL yang pertama. Pemeriksaan 1 menyebut sebab sebenarnya:
Batas_KPH tidak sah. Pemeriksaan 2, 3, 5, 6, dan 9 gagal sebagai akibat, dengan pesan yang tampak berbeda, misalnyaIndexErrorataubukan layer vektor. Pesan itu gejala, bukan sebab. - Yang tetap LULUS. Pemeriksaan 4, 7, 8, dan 10 tidak memakai layer batas, jadi tetap lulus. Itu bukti bahwa pemeriksaan berdiri sendiri.
- Skor dan saran.
SKOR: 4 dari 10. Ulangi: Bab 1, Bab 2, Bab 5.Dalam peta bab buku ini: Bab 14, Bab 21, dan Bab 22.
Bagian yang boleh diubah.
- Baris 13-14, jalur. Sesuaikan dengan komputer Anda.
- Baris 60, 62, dan 100, angka harapan. Angka 3, 4, 2.0, dan teks
"Titik api: 5 titik"hanya cocok untuk data paketpaket-i4. Ubah bila data Anda berbeda. - Baris 128-132, daftar pemeriksaan. Tambah pemeriksaan baru di sini.
- Baris 138-146, tugas ke-10. Ganti isi
kerjadengan pekerjaan yang ingin Anda uji.
Jebakan.
- Angka harapan terkunci pada satu paket data. Pada uji,
"Titik api: 5 titik"di baris 100 diganti"Titik api: 6 titik". Hasilnya6. Teks label dari ekspresi [Bab 5] GAGAL AssertionError: teks label salah: Titik api: 5 titik, dan skor9 dari 10. Ulangi: Bab 5.Ceklis menolak data yang berbeda, walau skripnya benar. proyek.clear()menghapus objek layer. Pemeriksaan 9 mengosongkan proyek, dan layer yang dibuka di baris 40-42 ikut terhapus. Pada uji, pemeriksaan baru ditambahkan setelah pemeriksaan 9 dan memakai layerhotspot. HasilnyaRuntimeError: wrapped C/C++ object of type QgsVectorLayer has been deleted, dan skor menjadi10 dari 11. Ulangi: Bab 1.Letakkan pemeriksaan baru sebelum pemeriksaan 9, atau buka layernya lagi.- Pemeriksaan 10 hanya menguji penjumlahan. Tugas latar belakang di baris 138-146 hanya menjumlahkan 1 sampai 5. Lulus berarti
QgsTaskberjalan. Itu tidak membuktikan Anda bisa membuat batch sungguhan seperti Skrip 23.4. - Lulus 10 dari 10 bukan berarti mahir. Ceklis hanya memeriksa sepuluh keterampilan dengan satu paket data. Anggap ini tanda bekal cukup, bukan sertifikat.
- Proyek Anda dikosongkan. Baris 18 dan 122 menghapus semua layer di proyek terbuka. Jalankan di proyek kosong.
Latihan variasi.
- Tambahkan pemeriksaan ke-11: jumlah seluruh titik panas harus 12.
- Ubah harapan pemeriksaan 6 agar gagal, lalu baca skornya.
Contoh jawaban nomor 1 (sudah diuji). Tulis fungsi ini sebelum baris 128.
def k11():
tegaskan(sum(1 for _ in hotspot.getFeatures()) == 12, "titik panas bukan 12")
return "12 titik panas"Lalu tambahkan ("11. Jumlah titik panas [Bab 1]", k11), ke daftar, tepat sebelum pasangan k9, sesuai Jebakan di atas. Layar menampilkan 11. Jumlah titik panas [Bab 1] LULUS 12 titik panas di antara pemeriksaan 8 dan 9, mengikuti urutan di daftar. Skor akhirnya SKOR: 11 dari 11. Siap masuk M1.
Contoh jawaban nomor 2 (sudah diuji): ganti "Titik api: 5 titik" di baris 100 dengan "Titik api: 6 titik". Layar menampilkan pemeriksaan 6 GAGAL dan SKOR: 9 dari 10. Ulangi: Bab 5.
Cek paham #
- Mengapa keluaran model di Skrip 23.2 diambil dengan
hasil["sempadan_per_kph"], dan apa yang terjadi bila label keluaran di Skrip 23.1 diubah? - Mengapa fungsi tugas di Skrip 23.4 membuka layernya sendiri, dan fungsi mana yang boleh menambah layer ke proyek?
- Pada hasil uji negatif Skrip 23.5, pemeriksaan 3 gagal dengan pesan "bukan layer vektor". Apa penyebab sebenarnya, dan di mana Anda membacanya?
Jawaban.
- Nama keluaran model dibentuk dari label keluarannya, yaitu "Sempadan per KPH" menjadi
sempadan_per_kph. Bila label diganti, nama itu ikut berubah, dan Skrip 23.2 berhenti karena tidak menemukan tempat hasil dengan nama lama. - Fungsi tugas berjalan di latar belakang dan tidak boleh menyentuh proyek. Karena itu ia membuka layernya sendiri. Fungsi
selesaiberjalan di program utama, jadi aman menambah layer. - Penyebabnya berkas batas tidak bisa dibuka karena nama berkasnya salah. Penyebab itu tertulis pada pemeriksaan 1, yaitu GAGAL yang pertama. Pemeriksaan 3 hanya gejala.
Kesalahan umum #
- Menjalankan Skrip 23.2 sebelum Skrip 23.1. Perbaikan: jalankan pembuat model lebih dulu, dan baca baris pertama hasil.
Model terbaca? Falseberarti berkas model tidak ada. - Menjalankan skrip bab ini di proyek yang penting. Skrip 23.3 dan 23.5 mengosongkan proyek terbuka, dan Skrip 23.4 menambah layer. Perbaikan: buka proyek kosong lebih dulu.
- Menunggu pesan galat di layar padahal tugas berjalan di latar belakang. Perbaikan: buka panel Log Messages bila hasil tidak muncul.
- Memakai layer tanpa memeriksa
isValid(). Perbaikan: periksa dulu, sepertimuat_rasterdi Skrip 23.3. Skrip 23.4 yang tidak memeriksa bisa menghasilkan angka ngawur tanpa galat. - Mengira skor 10 dari 10 sama dengan mahir. Perbaikan: pakai ceklis sebagai pengingat, lalu coba skrip sendiri pada data Anda.
Ringkasan dan latihan #
- Model Processing hanyalah berkas
.model3. Ia bisa dibuat dengan klik atau dengan kode, lalu dijalankan berulang dari Python denganprocessing.run(model, {...}). - Penjaga di dalam model menghentikan pekerjaan dengan pesan jelas bila isian tidak masuk akal.
- Proyek terpadu memadukan pola dari bab sebelumnya: pembantu, tahap demi tahap, tabel ringkasan, GeoPackage, dan proyek.
- Tugas latar belakang menjaga QGIS tetap hidup. Galatnya hanya masuk ke log, jadi periksa log bila hasil tidak muncul.
- Ceklis otomatis memakai pola
periksa,tegaskan, dan daftar fungsi. Uji negatif membuktikan ceklis bisa berkata GAGAL.
Latihan. Buat salinan Skrip 23.5, lalu tambahkan pemeriksaan ke-11 untuk skrip yang menurut Anda paling penting dari bab ini. Tulis syarat yang benar-benar bisa gagal, misalnya hasil model 23.2 untuk 30 meter harus 4,07 ha. Uji dua kali: sekali dengan data benar, sekali dengan data yang sengaja Anda rusak.
Tabel perbandingan: empat cara menjalankan pekerjaan yang sama #
| Cara | Kelebihan | Kekurangan | Skrip |
|---|---|---|---|
| Model Designer (klik) | Tanpa kode, bisa dilihat sebagai gambar | Sulit diulang dengan banyak nilai | Buku I4, Bab 6 |
| Model dari Python | Bisa diulang dengan perulangan dan diuji | Perlu hafal nama kelas model | 23.1 dan 23.2 |
| Skrip terpadu | Satu jalan dari data sampai proyek | QGIS terasa macet saat bekerja | 23.3 |
| Tugas latar belakang | QGIS tetap bisa dipakai | Galat hanya tampak di log | 23.4 |