Lewati ke isi
Profil penulisSeri Buku GIS Kehutanan dan Pertanian/ PY

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.

Ilustrasi 23.1: Model: masukan, penjaga, langkah, keluaran
Skema model sempadan sungai: tiga masukan, satu penjaga yang menghentikan model bila jarak tidak sah, tiga langkah alat, dan satu keluaran

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.
Ilustrasi 23.2: Satu berkas model, dua cara membuat, dua cara memakai
Skema berkas model3 yang bisa dibuat dengan klik atau dengan Python, dan dipakai dari menu atau dari Python

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.

Ilustrasi 23.3: Alur skrip 23.1
Skema enam tahap: buat model, tiga masukan, empat pembantu, penjaga, rantai buffer irisan luas, simpan dan cetak

Skrip utuh:

PYTHONUnduh berkas
# [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).

BarisTulisanArtinya
1-3# [SKRIP 6.1 ...Komentar. Baris 3 menyebut bahwa model ini sama dengan yang bisa Anda gambar di Model Designer.
4import osPerkakas untuk urusan folder (Bab 8).
5-8from qgis.core import (...)Panggil perkakas pembuat model. QgsProcessingModelChildParameterSource as Sumber memberi nama panjang itu julukan pendek Sumber. Julukan seperti ini disebut alias.
10-12DATA, HASIL, os.makedirs(...)Dua alamat, lalu buat folder hasil bila belum ada. DATA tidak dipakai skrip ini.
14m = 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.

BarisTulisanArtinya
17-18m.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-20masukan SUNGAISama, tetapi menerima layer garis.
21-23masukan JARAKMasukan 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.

BarisTulisanArtinya
26-30def anak(alat, deskripsi):Buat satu langkah. generateChildId(m) memberinya nomor pengenal yang unik di model, dan setDescription memberi nama yang terbaca manusia.
33-34def dari_input(nama):Sumber nilai dari masukan model. Dikembalikan dalam daftar.
37-38def nilai(v):Sumber nilai berupa nilai tetap.
41-42def 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).

BarisTulisanArtinya
46henti = anak("native:raiseexception", ...)Buat langkah dari alat Raise exception, yaitu alat yang sengaja menghentikan model dengan pesan buatan Anda.
47henti.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.
49m.addChildAlgorithm(henti)Pasang langkah ini ke model.

Blok 5: buffer dan irisan (baris 51-62).

BarisTulisanArtinya
52-55buf = anak("native:buffer", ...) lalu tiga addParameterSourcesBuat langkah Buffer. INPUT diambil dari masukan SUNGAI, DISTANCE dari masukan JARAK, dan DISSOLVE bernilai tetap benar (hasil digabung).
56buf.setDependencies([QgsProcessingModelChildDependency(henti.childId())])Syarat urutan: Buffer baru boleh jalan setelah penjaga lolos. Tanpa baris ini, QGIS tidak tahu bahwa penjaga harus lebih dulu.
57m.addChildAlgorithm(buf)Pasang ke model.
59-62iris = anak("native:intersection", ...)Langkah Irisan. INPUT dari masukan KPH, OVERLAY dari keluaran Buffer (dari_anak(buf)).

Blok 6: luas dan keluaran (baris 64-75).

BarisTulisanArtinya
64-69luas = 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 tetapRumus 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-74keluaran = QgsProcessingModelOutput("SEMPADAN", "Sempadan per KPH")Tandai hasil langkah ini sebagai keluaran model. setChildOutputName("OUTPUT") menyebut keluaran alat mana yang dipakai, dan setModelOutputs memasangnya.
75m.addChildAlgorithm(luas)Pasang langkah terakhir.

Blok 7: simpan dan lapor (baris 77-80).

BarisTulisanArtinya
77-78m.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):

HASIL
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 <= 0 boleh diganti, misalnya @JARAK <= 0 or @JARAK > 500.
  • Baris 55, nilai(True). Ganti False bila 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 memakai sempadan_per_kph, berhenti dengan Could 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.
  • DATA tidak 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 sebabnya FORMULA berupa nilai tetap. [CEK: bagian ini tidak diuji ulang di bab ini]

Latihan variasi.

  1. Ganti pesan penjaga menjadi "Jarak harus lebih besar dari nol."
  2. Ubah nilai bawaan JARAK menjadi 50.

Contoh jawaban nomor 1 (sudah diuji): ubah baris 47 menjadi

PYTHON
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.

Ilustrasi 23.4: Alur skrip 23.2
Skema tiga tahap: muat model, jalankan tiga kali dengan lebar 20, 30, dan 50 meter, uji jarak nol

Skrip utuh:

PYTHONUnduh berkas
# [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.

BarisTulisanArtinya
4-5import processing dan from qgis.core import ...Perkakas untuk memanggil alat, membuka layer, memuat model, dan menangkap QgsProcessingException.
10-11batas = ... dan sungai = ...Buka dua layer masukan.
14-15model = QgsProcessingModelAlgorithm() lalu model.fromFile(...)Buat model kosong, lalu isi dari berkas. fromFile menjawab True bila berkas terbaca, dan False bila tidak.
16model.initAlgorithm({})Siapkan model supaya isiannya terdaftar. Tanpa baris ini, daftar isian kosong.
17-18displayName() dan parameterDefinitions()Cetak nama model dan nama semua isiannya.
21for 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-24processing.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.
25lyr = hasil["sempadan_per_kph"]Ambil layer hasil memakai nama keluaran yang Anda lihat di Skrip 23.1. Huruf kecilnya harus persis.
26total = sum(f["LUAS_HA"] for f in lyr.getFeatures())Jumlahkan kolom LUAS_HA dari semua potongan.
27print("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-35try: ... 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):

HASIL
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, dan Parameter: [], lalu skrip berhenti dengan KeyError: '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". Hasilnya KeyError: 'Sempadan_per_KPH'. Salin nama dari daftar Parameter:.
  • Keluaran tampak sebagai isian. Daftar Parameter: memuat sempadan_per_kph walau 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.

  1. Ganti daftar jarak menjadi 10, 25, 40, dan 100.
  2. 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.

PYTHON
    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.

Ilustrasi 23.5: Alur skrip 23.3
Skema delapan bagian: siapkan proyek, pembantu, muat enam layer, data lapangan, analisis vektor, analisis raster, tabel ringkasan, kemas dan simpan

Skrip utuh:

PYTHONUnduh berkas
# [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).

BarisTulisanArtinya
1-3komentarJudul, penulis, dan daftar keluaran.
4-8import ... dan from qgis.core import ...Perkakas yang sudah Anda kenal: csv, glob, os, processing, serta layer vektor, layer raster, dan CRS.
10-12DATA, HASIL, os.makedirs(...)Dua alamat dan pembuat folder hasil.
14-15CRS dan SEMENTARADua nilai yang dipakai berulang, ditulis sekali di atas. Ganti di satu tempat, berlaku di semua tempat.
16-19proyek = QgsProject.instance() dan tiga baris sesudahnyaAmbil 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.

BarisTulisanArtinya
23-27def muat_vektor(jalur, nama):Buka layer vektor. Bila tidak sah, raise ValueError.
30-34def muat_raster(jalur, nama):Sama untuk raster, dengan penyedia data "gdal".
37-39def 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-46def 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).

BarisTulisanArtinya
49-54enam 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-56for 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.

BarisTulisanArtinya
59-60tujuan_crs = ... dan diterima, log = [], []Siapkan CRS tujuan dan dua daftar kosong sekaligus dalam satu baris.
61-73for 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.
75gabung = jalankan("native:mergevectorlayers", ...)Gabungkan semua kiriman yang diterima.
76-78komentar dan native:deletecolumnTiap 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-80survei.setName(...) lalu addMapLayerBeri nama Titik_Survei_Gabungan dan tampilkan. Nama ini dipakai rumus di baris 101.
81-82with open(... "log_kiriman.csv" ...)Tulis log. [("berkas", "status")] + log menyambung satu baris judul di depan daftar log.
83ditolak = 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-85print("Kiriman:", ...)Cetak ringkasan diterima dan ditolak.

Blok 4: analisis vektor (baris 87-103). Pola dari Bab 21 dan Skrip 22.4.

BarisTulisanArtinya
88-90sempadan = jalankan("native:buffer", ...)Sempadan 30 meter di kiri-kanan sungai, digabung jadi satu (DISSOLVE=True). Beri nama Sempadan_30m dan tampilkan.
91-92irisan = 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-95sempadan_ha = {} lalu perulanganJumlahkan 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.
98cacah = "aggregate('%s', 'count', \"%s\", intersects(...))"Cetakan rumus agregasi. Dua tanda %s nanti diisi nama layer dan nama kolom.
99-101tiga hitung_kolom(...) berantaiTambah kolom LUAS_HA, HOTSPOT, dan TITIK_SURVEI. Hasil tiap langkah menjadi masukan langkah berikutnya, dan cacah % (...) mengisi cetakan rumusnya.
102-103ringkas.setName(...) lalu addMapLayerBeri nama Ringkasan_KPH dan tampilkan.

Blok 5: analisis raster (baris 105-122). Pola dari Skrip 21.3.

BarisTulisanArtinya
106-107ndvi_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-109chm_jalur = jalankan("native:rastercalc", ...)Hitung tinggi tajuk (CHM) sebagai DSM dikurangi DTM, ke berkas CHM.tif.
110-113muat_raster(...) dan addMapLayerBuka kedua hasil dan tampilkan. Alat raster mengembalikan jalur teks, bukan layer.
116-119def 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.
122ndvi_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).

BarisTulisanArtinya
125-130judul = [...] dan baris = [] lalu perulanganSusun satu baris per KPH: nama, luas, sempadan, titik panas, titik survei, NDVI, dan tinggi tajuk, yang dibulatkan.
131-134with open(... "ringkasan_kph.csv" ...)Tulis ke CSV: baris judul, lalu semua baris.
135-137print("%-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).

BarisTulisanArtinya
141-143processing.run("native:package", {...})Tulis tiga layer sementara ke satu berkas GeoPackage. OVERWRITE: True menimpa berkas lama.
144-147for 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.
148proyek.write(HASIL + "/Proyek_Terpadu.qgz")Simpan proyek. Hasilnya True bila berhasil.
149print("Isi proyek:", sorted(...))Cetak daftar nama layer di proyek, urut abjad.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
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, kolom SEMPADAN_HA berubah.
  • Baris 99-101, kolom ringkasan. Tambah kolom baru dengan satu panggilan hitung_kolom lagi.
  • 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].
  • fid kembar. Pada uji, baris 78 diganti survei = gabung. Skrip berjalan sampai tabel tercetak, lalu berhenti di baris 142 dengan QgsProcessingException: Error obtained while packaging one or more layers. Di layar juga muncul ERROR 1: failed to execute insert : UNIQUE constraint failed: Titik_Survei_Gabungan.fid. Itu sebabnya kolom fid dibuang.
  • Nama layer di rumus harus sama. Pada uji, "Hotspot_Nasional" di baris 100 diganti "Hotspot". Skrip berhenti di baris 100 dengan Cannot 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_HA memakai $area (mengikuti ellipsoid proyek), sedangkan SEMPADAN_HA memakai geometry().area() (bidang datar). Bedanya sangat kecil, dan dibahas pada Skrip 22.4.

Latihan variasi.

  1. Ubah lebar sempadan menjadi 50 meter.
  2. Cetak tambahan: sempadan sebagai persen dari luas KPH.

Contoh jawaban nomor 1 (sudah diuji): ubah baris 88 dan 89 menjadi

PYTHON
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.

PYTHON
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.

Ilustrasi 23.6: Dua jalur kerja Skrip 23.4
Skema dua jalur: program utama mengirim tugas lalu tetap bisa dipakai, latar belakang menghitung dalam tiga tahap lalu mengembalikan daftar, dan fungsi selesai menampilkannya

Skrip utuh:

PYTHONUnduh berkas
# [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.

BarisTulisanArtinya
4-13impor, DATA, HASIL, SEMENTARASama seperti Skrip 23.3, ditambah QgsApplication, QgsTask, dan QgsMessageLog untuk tugas dan catatan.
16-17def jalankan(alat, **parameter):Pembantu yang sama seperti di Skrip 23.3.
20def hitung_ringkasan(task, data, hasil):Fungsi tugas. Isian pertama, task, dikirim QGIS otomatis. Dua isian lainnya berasal dari baris 94-95.
22-27enam pemanggilan QgsVectorLayer dan QgsRasterLayerLayer dibuka di dalam fungsi tugas, bukan diambil dari proyek. Fungsi tugas tidak boleh menyentuh proyek (komentar di baris 21).
28-30for 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-41tahap 1: sempadanBuffer dan irisan seperti Skrip 23.3. task.setProgress(25) melaporkan kemajuan 25 persen. if task.isCanceled(): return None berhenti bila pengguna membatalkan tugas.
43-49tahap 2: titik panasAlat 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-56tahap 3a: NDVI dan CHMDua native:rastercalc seperti Skrip 23.3. Kemajuan 75 persen.
58-62def rata(jalur, awalan):Fungsi di dalam fungsi, karena hanya dipakai di sini. Ia membuka raster hasil dan menghitung rata-rata per KPH.
64-65n, c = rata(...), rata(...) lalu task.setProgress(100)Hitung dua rata-rata, lalu lapor kemajuan penuh.
66-71hasil_baris = [] lalu return hasil_barisSusun 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()).
74def selesai(exception, baris=None):Fungsi selesai. exception berisi galat bila tugas gagal. baris berisi daftar dari return.
76-81if exception is not None: lalu if baris is None:Tiga jalur: gagal (catat Qgis.Critical), dibatalkan (catat Qgis.Warning), atau sukses.
82-85with open(... "ringkasan_latar.csv" ...)Tulis CSV. Berbeda dari Skrip 23.3, tidak ada kolom TITIK_SURVEI, karena versi ini tidak menggabung kiriman lapangan.
86-87for nama in ("NDVI_KPH", "CHM"):Tambahkan dua raster hasil ke proyek. Ini aman di sini karena fungsi selesai berjalan di program utama.
88-91for b in baris: print(...)Cetak satu baris per KPH, lalu catat Qgis.Success di log.
94-95tugas = 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-97addTask(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):

HASIL
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 m

Baris 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 sempadan berubah.
  • 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=DATA di baris 95 diubah menjadi data=DATA + "/salah". Layar hanya menampilkan Tugas berjalan di latar belakang; QGIS tetap bisa dipakai. dan tidak ada pesan lain. Galatnya hanya masuk ke log, dengan teks Ringkasan 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, teksnya Ringkasan 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 m atau CHM inf m, dan nilainya bisa berbeda tiap kali dijalankan. Pada uji, nama berkas DSM di baris 26 diganti DSM_Hilang.tif. Dengan pengaman, tugas berhenti, tidak ada CSV, dan log memuat Ringkasan 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.

  1. Ubah sempadan menjadi 50 meter.
  2. 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.

Ilustrasi 23.7: Sepuluh pemeriksaan Skrip 23.5
Skema sepuluh kotak pemeriksaan dengan label bab Buku I4 dan padanannya di buku ini

Skrip utuh:

PYTHONUnduh berkas
# [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).

BarisTulisanArtinya
5-11import ... dan from qgis.core import (...)Perkakas yang sudah dikenal, ditambah re untuk mencari pola teks dan time untuk jeda.
17-19proyek = ..., removeAllMapLayers(), catatan = []Ambil proyek, kosongkan, dan siapkan daftar catatan. Tiap catatan berisi nama, lulus atau tidak, dan alasan.
20-22NDVI dan CACAHDua rumus yang dipakai pemeriksaan, ditulis sekali di atas.
25-32def 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-37def tegaskan(syarat, pesan):Bila syarat salah, raise AssertionError(pesan). Pemeriksaan lain cukup memanggil fungsi ini.
40-42tiga layerBuka layer batas, titik panas, dan citra.
45-49def 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.

PemeriksaanBarisYang diujiAsal materi
152-56Tiga layer terbuka (isValid()), masuk proyek, jumlahnya 3Bab 14
259-64mapLayersByName memberi layer yang benar. Batas berisi 3 fitur, citra 4 band dan piksel 2 meter. Nama yang tidak ada memberi daftar kosongBab 14
367-74Hasil native:buffer berupa layer vektor, hasil native:rastercalc berupa jalur teks berkasBab 21
477-83NDVI dihitung dengan rastercalc, lalu nilainya harus antara -1 dan 1 (perbandingan berantai)Bab 21
586-93Agregasi @parent menghasilkan tiga angka, dan sama dengan hitungan manual memakai intersectsBab 22
696-101Teks label Titik api: 5 titik untuk KPH BetaBab 22
7104-110Alat dengan masukan yang tidak ada harus gagal. Bila tidak gagal, raise AssertionError("seharusnya gagal")Bab 22
8113-116Berkas rusak tim_e.gpkg memberi isValid() bernilai FalseBab 22
9119-125Tulis proyek, kosongkan (proyek.clear()), baca ulang, dan tiga layer kembaliBab 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).

BarisTulisanArtinya
128-132daftar = [("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-134for 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).

BarisTulisanArtinya
138-146def 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).

BarisTulisanArtinya
150-152def selesai(exception, nilai=None):Pemeriksaan ke-10 lulus bila tidak ada galat dan hasilnya 15. Catat dan cetak seperti pemeriksaan lain.
153lulus = sum(1 for _, o, _ in catatan if o)Hitung catatan yang lulus.
154gagal = 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-156akhir = ... lalu print("SKOR: ...")Bila tidak ada yang gagal, cetak "Siap masuk M1." Bila ada, cetak bab yang perlu diulang.
159-160QgsTask.fromFunction(...) lalu addTaskBuat dan kirim tugas, seperti Skrip 23.4.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2):

HASIL
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:

HASIL
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, misalnya IndexError atau bukan 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 paket paket-i4. Ubah bila data Anda berbeda.
  • Baris 128-132, daftar pemeriksaan. Tambah pemeriksaan baru di sini.
  • Baris 138-146, tugas ke-10. Ganti isi kerja dengan 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". Hasilnya 6. Teks label dari ekspresi [Bab 5] GAGAL AssertionError: teks label salah: Titik api: 5 titik, dan skor 9 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 layer hotspot. Hasilnya RuntimeError: wrapped C/C++ object of type QgsVectorLayer has been deleted, dan skor menjadi 10 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 QgsTask berjalan. 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.

  1. Tambahkan pemeriksaan ke-11: jumlah seluruh titik panas harus 12.
  2. Ubah harapan pemeriksaan 6 agar gagal, lalu baca skornya.

Contoh jawaban nomor 1 (sudah diuji). Tulis fungsi ini sebelum baris 128.

PYTHONUnduh berkas
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 #

  1. 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?
  2. Mengapa fungsi tugas di Skrip 23.4 membuka layernya sendiri, dan fungsi mana yang boleh menambah layer ke proyek?
  3. Pada hasil uji negatif Skrip 23.5, pemeriksaan 3 gagal dengan pesan "bukan layer vektor". Apa penyebab sebenarnya, dan di mana Anda membacanya?

Jawaban.

  1. 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.
  2. Fungsi tugas berjalan di latar belakang dan tidak boleh menyentuh proyek. Karena itu ia membuka layernya sendiri. Fungsi selesai berjalan di program utama, jadi aman menambah layer.
  3. 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? False berarti 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, seperti muat_raster di 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 dengan processing.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 #

CaraKelebihanKekuranganSkrip
Model Designer (klik)Tanpa kode, bisa dilihat sebagai gambarSulit diulang dengan banyak nilaiBuku I4, Bab 6
Model dari PythonBisa diulang dengan perulangan dan diujiPerlu hafal nama kelas model23.1 dan 23.2
Skrip terpaduSatu jalan dari data sampai proyekQGIS terasa macet saat bekerja23.3
Tugas latar belakangQGIS tetap bisa dipakaiGalat hanya tampak di log23.4