Lewati ke isi
Profil penulisSeri Buku GIS Kehutanan dan Pertanian/ I4
Tampilkan bagian untuk:

BAB 7: Bab Jembatan ke Master: Proyek Mini Terpadu dan Ceklis Masuk M1

#Studi kasus: "Laporan awal bulan, satu skrip"

Awal bulan tiba. Kepala Seksi minta laporan lengkap: kiriman lapangan yang sudah digabung, luas sempadan sungai dan titik panas per petak, serta kondisi vegetasi dari citra dan drone. Tahun lalu pekerjaan ini memakan dua hari. Hari ini Anda menjalankannya dalam beberapa detik. Sambil menunggu hasil, Anda membaca daftar isi Masterclass GIS Modern (M1): citra cloud, survei lapangan, drone, LiDAR, klasifikasi objek, dan atlas. Anda ingin tahu: apakah bekal Anda cukup?

#Konsep: jembatan dalam tiga kalimat

Bab Jembatan adalah proyek kecil yang memadukan semua yang Anda pelajari di kelas Intermediate, ditambah pemeriksaan bekal sebelum masuk kelas Master. Seperti ujian praktik sebelum turun ke jalan raya, proyek ini menguji tangan Anda, bukan hanya ingatan Anda. Setelah lulus ceklisnya, Anda siap menyeberang ke M1.

Istilah baru bab ini:

  • Proyek terpadu: satu skrip yang memadukan beberapa seri.
  • Keluaran permanen: hasil yang disimpan di GeoPackage atau berkas, bukan hanya di memori.
  • Ceklis masuk: daftar kemampuan yang M1 anggap sudah Anda kuasai.
Ilustrasi 7.1: Proyek mini terpadu
Skema: tiga bagian dari seri I3, I1, dan I2 bertemu dalam satu skrip yang menghasilkan ringkasan per KPH dan sebuah proyek

#Bagian A: QGIS

#Bagian A: Jembatan di QGIS

#A.1 Rancangan proyek

Proyek ini memakai data paket paket-i4. Tiap bagian memakai kemampuan dari satu seri dan satu bab di buku ini.

SeriPekerjaanAlat dan kemampuanBab di buku ini
I3Gabung enam kiriman lapangan, tolak yang rusak, ubah CRS, tandai titik di luar areaQgsVectorLayer, isValid, try-except, reprojectlayer, mergevectorlayersBab 1 dan 4
I1Sempadan sungai 30 m per KPH, titik panas, titik survei per KPHbuffer, intersection, fieldcalculator, aggregateBab 2 dan 5
I2NDVI, tinggi tajuk (CHM), rata-rata per KPHrastercalc, zonalstatisticsfbBab 2
PenutupRingkasan CSV, GeoPackage hasil, dan proyek .qgznative:package, QgsProject.writeBab 1

#A.2 Skrip terpadu

Skrip 7.1 berjalan dari atas ke bawah. Bagian 0 menyiapkan pembantu: fungsi memuat layer yang menolak masukan rusak, dan fungsi jalankan yang menyingkat pemanggilan alat. Setelah itu tiap bagian seri mengerjakan tugasnya.

PYTHON (PYQGIS) 7.1
# [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()))

Hasil uji di QGIS 4.0.2:

HASIL UJI SKRIP 7.1
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']

Seluruh angka dapat dicek silang. Luas petak 4, 4, dan 8 ha sesuai data. Sempadan 1,18, 1,39, dan 1,50 ha sama dengan Bab 2. Titik panas 2, 5, dan 5 sama dengan Bab 5. NDVI 0,604, 0,431, dan 0,329 sama dengan Bab 2. Titik survei 6, 4, dan 7 sesuai pembuat data: tim A mengirim 6 titik di KPH Alpha, tim B 4 titik di KPH Beta (satu titiknya di luar area), serta tim C dan D 4 + 3 titik di KPH Gamma. Total 17 titik di dalam area, ditambah 1 di luar, yaitu 18 titik hasil gabungan.

Sebagai bukti, proyek Proyek_Terpadu.qgz yang tersimpan dibuka ulang, dan kesembilan layer di dalamnya terbaca sah. Hasil lengkapnya dicatat di berkas catatan seri.

#A.3 Versi latar belakang

Pada data besar, pekerjaan ini bisa berjalan lama. Skrip 7.2 memindahkan bagian beratnya ke QgsTask. Bagian yang menyentuh proyek, yaitu menambah layer ke peta dan menulis laporan, dikerjakan di fungsi selesai.

PYTHON (PYQGIS) 7.2
# [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")

    # 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.")

Hasil uji di QGIS 4.0.2:

HASIL UJI SKRIP 7.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 m

Angkanya sama dengan Skrip 7.1. Itu bukti bahwa memindahkan pekerjaan ke latar belakang tidak mengubah hasil.

#A.4 Peta celah: apa yang M1 pakai, dan di mana Anda belajar

Ilustrasi 7.2: Menutup celah sebelum masuk Masterclass
Skema: enam kemampuan dari seri I4 di kiri menghubungkan ke lima bab M1 di kanan lewat bab jembatan dan ceklis masuk

Tabel berikut memetakan kebutuhan skrip dan ekspresi di M1 ke bab buku ini. Isi M1 tidak berubah. Bila pembahasan sebuah topik memang ada di M1, tabel menyebutnya.

Bab M1Yang dipakai skripnyaDipelajari di
Bab 1QgsProject.instance(), QgsRasterLayer(url, nama, "gdal"), mapLayersByName(...)[0]Bab 1 buku ini
Bab 1processing.run(...) dengan TEMPORARY_OUTPUT, gdal:cliprasterbymasklayer, native:rastercalcBab 2 dan Skrip 3.2
Bab 1Pengenalan STAC, COG, dan awalan /vsicurl/M1 Bab 1 sendiri
Bab 2native:joinattributesbylocation, perulangan fitur, hitung persen cocokBab 4 (Skrip 4.2)
Bab 3CHM dari DSM dikurangi DTM, QgsTask.fromFunction, batch folderBab 2 dan 3
Bab 4Point cloud dan alat PDALM1 Bab 4 sendiri
Bab 5Statistik zonalBab 2 (A.4); klasifikasi OBIA dan Random Forest di M1 sendiri
Bab 6Model dengan Conditional branch, ekspresi aggregate dengan @parent di label atlasBab 5 dan 6; layout dan atlas dari seri B4 dan M1

Satu catatan praktis. Skrip contoh di internet atau buku lain kadang memakai ID alat atau bentuk keluaran lama. Bila sebuah skrip gagal, periksa dua hal lebih dulu: nama ID alat (Skrip 2.1) dan jenis keluaran sementara (tabel di Bab 2, A.2). Kedua hal itu penyebab gagal yang paling sering ditemukan pada uji.

#A.5 Ceklis masuk M1

Centang dalam hati, atau di kertas. Tiap butir berarti: Anda bisa melakukannya tanpa melihat buku, minimal pada data latihan.

Dari seri sebelumnya (konsep):

  • Saya bisa menentukan zona UTM dan mengganti CRS proyek. (Seri B1)
  • Saya paham band dan rumus NDVI. (Seri B3 dan I2)
  • Saya bisa membuat GeoPackage, domain nilai, dan formulir survei. (Seri B1 dan I3)
  • Saya bisa menjalankan spatial join dan statistik zonal dengan klik. (Seri I1 dan I2)
  • Saya bisa membuat layout peta sederhana. (Seri B4)

Dari buku ini (praktik, tiap butir ada skripnya):

  • Saya bisa membuka Python Console dan menjalankan skrip dari editor. (Bab 1)
  • Saya bisa memuat layer vektor dan raster lewat skrip, lalu mengambilnya dengan nama. (Bab 1)
  • Saya bisa menjalankan alat Processing dengan processing.run dan memakai TEMPORARY_OUTPUT. (Bab 2)
  • Saya tahu bahwa keluaran raster sementara berupa teks jalur, dan bagaimana membungkusnya menjadi layer. (Bab 2)
  • Saya bisa menulis perulangan untuk banyak berkas atau fitur. (Bab 3)
  • Saya bisa memindahkan pekerjaan berat ke QgsTask. (Bab 3)
  • Saya bisa menangkap galat dan membuat laporan diterima atau ditolak. (Bab 4)
  • Saya bisa menulis ekspresi dasar, termasuk aggregate dengan @parent. (Bab 5)
  • Saya bisa menyusun model dengan isian dan penjaga, lalu menjalankannya. (Bab 6)

Uji otomatis. Skrip 7.3 menguji sepuluh kemampuan di atas sekaligus. Tiap pemeriksaan mencetak LULUS atau GAGAL beserta alasannya. Skor akhir muncul setelah tugas latar belakang terakhir selesai.

PYTHON (PYQGIS) 7.3
# [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)

Hasil uji di QGIS 4.0.2:

HASIL UJI SKRIP 7.3
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.

Bila ada pemeriksaan yang GAGAL, skrip menyebut bab yang perlu diulang. Pada uji, nama satu berkas data sengaja dibuat salah, dan skor turun menjadi 4 dari 10 dengan pesan Ulangi: Bab 1, Bab 2, Bab 5. Skor 10 dari 10 berarti bekal teknis Anda cukup. Skor itu tidak menggantikan latihan nyata di data Anda sendiri.

#A.6 Yang akan Anda pelajari di M1

Beberapa hal memang baru di M1, dan Anda tidak perlu menguasainya sekarang: membaca citra dari cloud (STAC dan COG), plugin STAC API Browser, peta dasar XYZ Tiles, sinkronisasi lapangan lewat QField atau Mergin Maps, pencarian puncak pohon, point cloud, Orfeo Toolbox untuk klasifikasi objek, dan atlas laporan. Kemampuan di buku ini menjadi fondasi untuk semuanya.

#Bagian B: ArcGIS Pro

#Bagian B: Jembatan di ArcGIS Pro

Proyek terpadu versi ArcPy memakai tiga alat Bab 2 (Buffer, Intersect, Spatial Join), dua perhitungan raster (NDVI dan CHM), dan Zonal Statistics as Table. Penjaganya adalah blok try-except-finally dari Bab 4. Skrip menulis ringkasan CSV.

PYTHON (ARCPY, PYTHON 3)
# [SKRIP 7.1-Pro: Proyek mini terpadu dengan ArcPy: ringkasan per KPH]
# Penulis: Badar Mubarok Yogaswara
# [CEK] Cocokkan dengan versi Anda. Butuh Spatial Analyst. Data Shapefile dipakai agar sejajar dengan Bagian QGIS.
import csv
import arcpy
from arcpy.sa import Raster, Float, ZonalStatisticsAsTable

DATA = r"D:\Latihan\paket-i4"
HASIL = r"D:\Latihan\hasil_i4"
arcpy.env.workspace = DATA + r"\shp"
arcpy.env.overwriteOutput = True
arcpy.CheckOutExtension("Spatial")

try:
    # I1: sempadan 30 m dan titik panas
    arcpy.analysis.Buffer("Sungai.shp", r"memory\sem", "30 Meters", dissolve_option="ALL")
    arcpy.analysis.Intersect(["Batas_KPH.shp", r"memory\sem"], r"memory\iris")
    arcpy.analysis.SpatialJoin("Batas_KPH.shp", "Hotspot_Nasional.shp", r"memory\ringkas",
                               "JOIN_ONE_TO_ONE", "KEEP_ALL", match_option="INTERSECT")
    # I2: NDVI dan CHM, lalu rata-rata per KPH
    nir = Float(Raster(DATA + r"\Citra_KPH.tif\Band_4"))
    merah = Float(Raster(DATA + r"\Citra_KPH.tif\Band_1"))
    ndvi = (nir - merah) / (nir + merah)
    chm = Raster(DATA + r"\DSM_Drone.tif") - Raster(DATA + r"\DTM_Drone.tif")
    ZonalStatisticsAsTable("Batas_KPH.shp", "NAMA_KPH", ndvi, r"memory\z_ndvi", "DATA", "MEAN")
    ZonalStatisticsAsTable("Batas_KPH.shp", "NAMA_KPH", chm, r"memory\z_chm", "DATA", "MEAN")

    def tabel(jalur, kolom):
        with arcpy.da.SearchCursor(jalur, ["NAMA_KPH", kolom]) as k:
            return {n: v for n, v in k}

    sempadan = {}
    with arcpy.da.SearchCursor(r"memory\iris", ["NAMA_KPH", "SHAPE@AREA"]) as k:
        for n, luas in k:
            sempadan[n] = sempadan.get(n, 0) + luas / 10000
    n_rata, c_rata = tabel(r"memory\z_ndvi", "MEAN"), tabel(r"memory\z_chm", "MEAN")
    with open(HASIL + r"\ringkasan_kph_pro.csv", "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["NAMA_KPH", "LUAS_HA", "SEMPADAN_HA", "HOTSPOT", "NDVI_RATA", "CHM_RATA_M"])
        with arcpy.da.SearchCursor(r"memory\ringkas", ["NAMA_KPH", "SHAPE@AREA", "Join_Count"]) as k:
            for n, luas, jml in k:
                w.writerow([n, round(luas / 10000, 2), round(sempadan.get(n, 0), 2), jml,
                            round(n_rata[n], 3), round(c_rata[n], 2)])
    print("Ringkasan tersimpan.")
except arcpy.ExecuteError:
    print("Alat gagal:", arcpy.GetMessages(2))
finally:
    arcpy.CheckInExtension("Spatial")

Bagian B di M1 juga memakai ArcPy pada beberapa bab. Bekal dari buku ini cukup untuk membacanya: kursor, alat geoprocessing, ExecuteError, dan ruang kerja memory.

#Bagian C: ArcMap 10.8

#Bagian C: Jembatan di ArcMap 10.8

Versi ArcMap memakai nama alat lama dan Python 2.7. Bentuknya sejajar dengan Bagian B.

PYTHON (ARCPY, PYTHON 2.7)
# -*- coding: utf-8 -*-
# [SKRIP 7.1-ArcMap: Proyek mini terpadu dengan ArcPy: ringkasan per KPH (Python 2.7)]
# Penulis: Badar Mubarok Yogaswara
# [CEK] Cocokkan dengan versi Anda. Butuh Spatial Analyst.
import csv
import arcpy
from arcpy.sa import Raster, Float, ZonalStatisticsAsTable

DATA = r"D:\Latihan\paket-i4"
HASIL = r"D:\Latihan\hasil_i4"
arcpy.env.workspace = DATA + r"\shp"
arcpy.env.overwriteOutput = True
arcpy.CheckOutExtension("Spatial")

try:
    # I1: sempadan 30 m dan titik panas
    arcpy.Buffer_analysis("Sungai.shp", r"in_memory\sem", "30 Meters", dissolve_option="ALL")
    arcpy.Intersect_analysis(["Batas_KPH.shp", r"in_memory\sem"], r"in_memory\iris")
    arcpy.SpatialJoin_analysis("Batas_KPH.shp", "Hotspot_Nasional.shp", r"in_memory\ringkas",
                               "JOIN_ONE_TO_ONE", "KEEP_ALL", match_option="INTERSECT")
    # I2: NDVI dan CHM, lalu rata-rata per KPH
    nir = Float(Raster(DATA + r"\Citra_KPH.tif\Band_4"))
    merah = Float(Raster(DATA + r"\Citra_KPH.tif\Band_1"))
    ndvi = (nir - merah) / (nir + merah)
    chm = Raster(DATA + r"\DSM_Drone.tif") - Raster(DATA + r"\DTM_Drone.tif")
    ZonalStatisticsAsTable("Batas_KPH.shp", "NAMA_KPH", ndvi, r"in_memory\z_ndvi", "DATA", "MEAN")
    ZonalStatisticsAsTable("Batas_KPH.shp", "NAMA_KPH", chm, r"in_memory\z_chm", "DATA", "MEAN")

    def tabel(jalur, kolom):
        with arcpy.da.SearchCursor(jalur, ["NAMA_KPH", kolom]) as k:
            return dict((n, v) for n, v in k)

    sempadan = {}
    with arcpy.da.SearchCursor(r"in_memory\iris", ["NAMA_KPH", "SHAPE@AREA"]) as k:
        for n, luas in k:
            sempadan[n] = sempadan.get(n, 0) + luas / 10000.0
    n_rata, c_rata = tabel(r"in_memory\z_ndvi", "MEAN"), tabel(r"in_memory\z_chm", "MEAN")
    with open(HASIL + r"\ringkasan_kph_arcmap.csv", "wb") as f:      # Python 2: mode "wb" untuk csv
        w = csv.writer(f)
        w.writerow(["NAMA_KPH", "LUAS_HA", "SEMPADAN_HA", "HOTSPOT", "NDVI_RATA", "CHM_RATA_M"])
        with arcpy.da.SearchCursor(r"in_memory\ringkas", ["NAMA_KPH", "SHAPE@AREA", "Join_Count"]) as k:
            for n, luas, jml in k:
                w.writerow([n, round(luas / 10000.0, 2), round(sempadan.get(n, 0), 2), jml,
                            round(n_rata[n], 3), round(c_rata[n], 2)])
    print("Ringkasan tersimpan.")
except arcpy.ExecuteError:
    print("Alat gagal: %s" % arcpy.GetMessages(2))
finally:
    arcpy.CheckInExtension("Spatial")

ArcMap tidak punya padanan langsung QgsTask dan aggregate. Padanan yang tidak ada ditulis terus terang di tabel perbandingan, seperti di M1.

#Cek paham

  1. Mengapa kolom fid dibuang sebelum menyimpan hasil gabungan ke GeoPackage?
  2. Mengapa layer sementara diganti dengan layer dari GeoPackage sebelum proyek disimpan?
  3. Pada skrip QgsTask, bagian mana yang dikerjakan di fungsi selesai dan mengapa?

Jawaban:

  1. Setiap kiriman membawa nomor fid sendiri sehingga setelah digabung ada nomor kembar. GeoPackage menolak nomor kembar saat menyimpan.
  2. Layer sementara hilang saat QGIS ditutup. Layer dari GeoPackage tetap ada, jadi proyek bisa dibuka lagi dengan utuh.
  3. Menambah layer ke proyek dan menulis laporan. Bagian ini menyentuh proyek, sedangkan latar belakang tidak boleh menyentuhnya.

#Kesalahan umum

  • Proyek disimpan dengan layer sementara. Saat dibuka lagi, layer itu kosong atau hilang. Simpan ke GeoPackage dulu.
  • Mengabaikan titik yang ditandai curiga. Titik di luar area biasanya salah ketik koordinat. Periksa sebelum dianalisis.
  • Menyentuh proyek dari latar belakang. Pindahkan ke fungsi selesai.
  • Mengira skor 10 dari 10 berarti ahli. Skor hanya menandakan bekal dasar sudah cukup untuk melanjutkan.

#Ringkasan dan latihan

Ringkasan: proyek terpadu memadukan I3 (kiriman lapangan), I1 (analisis vektor), dan I2 (analisis raster) dalam satu skrip dengan pemeriksaan, laporan, dan penyimpanan permanen. Skrip yang sama dapat berjalan di latar belakang. Ceklis masuk M1 memuat sepuluh kemampuan, dan Skrip 7.3 mengujinya otomatis.

Latihan:

  1. Tambahkan ke Skrip 7.1 kolom TEKS_LAPORAN yang berisi kalimat seperti "KPH Beta: 4 ha, 5 titik panas, NDVI 0,43". Gunakan ekspresi dengan ||.
  2. Ganti sempadan 30 m menjadi isian di atas skrip (satu variabel LEBAR_SEMPADAN), lalu jalankan dengan 20 m. Total sempadan seharusnya 2,75 ha.
  3. Jalankan Skrip 7.3. Rusak sengaja satu hal (misalnya ubah DATA ke folder yang salah) dan lihat pemeriksaan mana yang GAGAL.

#Tabel perbandingan: kemampuan masuk M1

KemampuanQGISArcGIS ProArcMap 10.8
Muat dan ambil layerQgsProject, mapLayersByNamearcpy.mp, listLayers [CEK]arcpy.mapping, ListLayers [CEK]
Keluaran sementaraTEMPORARY_OUTPUTmemory\nama [CEK]in_memory\nama [CEK]
Perulangan batchfor + globfor + ListRasters [CEK]for + ListRasters [CEK]
Tugas latar belakangQgsTask (diuji)Tidak ada padanan langsung [CEK]Tidak ada padanan langsung [CEK]
Penanganan galattry-except + QgsProcessingExceptiontry-except + ExecuteError [CEK]try-except + ExecuteError [CEK]
Ekspresi agregasiaggregate(...)Arcade atau Spatial Join [CEK]Spatial Join [CEK]
Model alur kerjaModel DesignerModelBuilder [CEK]ModelBuilder [CEK]