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

BAB 17: Dari Data ke Lembar Peta: Mewarnai, Memberi Label, dan Mencetak dengan Skrip

Studi kasus: "Peta volume kayu untuk rapat besok" #

Kepala Seksi minta peta volume kayu per petak untuk rapat besok pagi. Anda bisa mengklik menu satu per satu: warna, label, layout, ekspor. Hasilnya bagus. Tetapi bulan depan data volume berubah, dan semua klik harus diulang.

Dengan skrip, Anda menyusun peta itu sekali. Bulan depan Anda cukup menjalankannya lagi, dan peta baru jadi dalam hitungan detik.

Konsep: peta jadi dalam tiga kalimat #

Peta yang enak dibaca dirakit bertahap, seperti menyusun poster. Layer adalah bahan, simbologi adalah cat, label adalah tulisan, layout adalah lembar posternya, dan ekspor adalah mencetaknya. Skrip PyQGIS mengerjakan tiap tahap itu dengan beberapa baris.

Ilustrasi 17.1: Lima tahap merakit peta
Skema lima tahap dari data ke peta jadi: data, simbologi, label, layout, ekspor, beserta skrip yang mengerjakannya

Lima istilah yang perlu Anda kenal sekarang:

  • Simbologi (symbology): aturan warna dan bentuk gambar sebuah layer di peta.
  • Penyaji (renderer): objek QGIS yang memegang aturan simbologi satu layer.
  • Simbol: satu tampilan, misalnya satu isian poligon cokelat dengan garis tepi tipis.
  • Label: tulisan di atas fitur peta, misalnya kode petak.
  • Layout: lembar peta untuk dicetak, berisi peta, judul, legenda, skala, dan panah utara.

Sebelum mulai: urutan menjalankan dan bahan #

Skrip di bab ini tidak berdiri sendiri semua. Beberapa skrip mengambil layer yang dibuat skrip sebelumnya, lewat nama layer di proyek. Karena itu urutannya penting. Tabel berikut merangkumnya.

SkripBerkasSyarat sebelum dijalankan
17.1skrip_4_1_kategori.pyTidak ada. Memuat layer sendiri.
17.2skrip_4_2_klasifikasi.pyJalankan 17.1 lebih dulu, di proyek yang sama.
17.3skrip_4_2b_raster.pyTidak ada, tetapi sebaiknya di proyek terpisah (lihat Jebakan 17.3).
17.4skrip_4_3_label.pyJalankan 17.1 lebih dulu.
17.5skrip_4_4_layout.pyJalankan 17.1, 17.2, dan 17.4 di proyek yang sama. Jangan jalankan 17.3.
17.6skrip_4_5_ekspor.pyJalankan 17.5 lebih dulu.
17.7skrip_4_6_jembatan.pyTidak ada, tetapi mengosongkan proyek. Jalankan di proyek kosong.

Menjalankan. Buka Konsol Python QGIS atau Editor Skrip, seperti yang Anda siapkan di Bab 3. Tempel isi skrip, lalu jalankan. Semua skrip memakai satu folder kerja, yaitu isi variabel FOLDER.

Bahan. Buat satu folder kerja, misalnya D:/KPH_Contoh. Salin lima berkas ke dalamnya.

  • Dari paket-b4: Petak_KPH.gpkg (layer Petak_KPH, 16 petak) dan Sungai_Jalan.gpkg (layer Sungai dan Jalan).
  • Dari paket-data: Hotspot_Nasional.gpkg (12 titik panas) dan DTM_Drone.tif (peta tinggi).
  • Dari paket-b1: Hotspot_derajat.gpkg. Hanya Skrip 17.7 yang memakainya.

Di setiap skrip, FOLDER tertulis r"D:/KPH_Contoh", yaitu jalur komputer penulis. Ganti dengan jalur folder Anda. Gunakan garis miring biasa /, seperti dijelaskan di Bab 8.

Pengujian. Semua hasil di bab ini dijalankan di QGIS 4.0.2 tanpa jendela utama (mode tanpa tampilan). Urutan uji: 17.1, 17.2, 17.4, 17.5, 17.6 dalam satu proyek. Skrip 17.3 dan 17.7 diuji sendiri-sendiri. Hasil tampilan visual dibaca dari berkas PDF dan PNG yang dihasilkan.

Skrip 17.1: Muat layer dan beri warna per jenis #

Berkas skrip_4_1_kategori.py sama dengan yang dipakai di Buku B4. Di dalam berkasnya tertulis "Skrip 4.1" mengikuti penomoran buku itu.

Tujuan. Memuat empat layer KPH Contoh, mewarnai petak menurut jenis tanaman, lalu memberi gaya pada sungai, jalan, dan titik panas.

Bahan. Folder kerja berisi Petak_KPH.gpkg, Sungai_Jalan.gpkg, dan Hotspot_Nasional.gpkg. Skrip ini memuat layer sendiri, jadi tidak butuh skrip lain lebih dulu. Proyek yang sedang terbuka langsung berubah.

Ilustrasi 17.2: Perbedaan Categorized dan Graduated
Skema dua cara mewarnai: Categorized memberi satu warna untuk tiap nilai teks pada kolom Jenis, Graduated memberi satu warna untuk tiap rentang angka pada kolom Volume_m3ha

Gambar di atas menunjukkan dua cara mewarnai. Skrip ini memakai cara kiri: Categorized, satu nilai teks satu warna. Cara kanan, Graduated, dipakai di Skrip 17.2.

Skrip ini bekerja dalam delapan tahap. Gambar berikut menunjukkan urutannya, lengkap dengan nomor baris.

Ilustrasi 17.3: Alur skrip 17.1
Skema delapan tahap skrip: atur CRS proyek, fungsi muat, muat empat layer, kamus warna, daftar kategori, pasang penyaji Categorized, simbol untuk sungai jalan dan titik panas, gambar ulang dan cetak

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.1: memuat empat layer KPH Contoh, lalu menata simbologinya.
# Petak diwarnai per jenis tanaman (Categorized); garis dan titik memakai Single Symbol.
from qgis.core import (QgsProject, QgsVectorLayer, QgsFillSymbol, QgsLineSymbol, QgsMarkerSymbol,
                       QgsRendererCategory, QgsCategorizedSymbolRenderer, QgsSingleSymbolRenderer,
                       QgsCoordinateReferenceSystem)

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda
proyek = QgsProject.instance()
proyek.setCrs(QgsCoordinateReferenceSystem("EPSG:32749"))   # CRS proyek: UTM 49S (meter)

def muat(berkas, lapisan, nama):
    layer = QgsVectorLayer("%s/%s|layername=%s" % (FOLDER, berkas, lapisan), nama, "ogr")
    assert layer.isValid(), "Layer tidak terbaca: " + berkas
    proyek.addMapLayer(layer)          # layer yang ditambah belakangan berada di atas
    return layer

petak = muat("Petak_KPH.gpkg", "Petak_KPH", "Petak_KPH")
sungai = muat("Sungai_Jalan.gpkg", "Sungai", "Sungai")
jalan = muat("Sungai_Jalan.gpkg", "Jalan", "Jalan")
titik = muat("Hotspot_Nasional.gpkg", "Hotspot_Nasional", "Hotspot_Nasional")

# Petak: satu warna untuk tiap jenis
warna = {"Jati": "#8C6D31", "Mahoni": "#B5523B", "Sengon": "#7FB069",
         "Akasia": "#D9A400", "Agroforestri": "#4F9DA6"}
kategori = []
for nama, hexa in warna.items():
    simbol = QgsFillSymbol.createSimple({"color": hexa, "outline_color": "#404040", "outline_width": "0.3"})
    kategori.append(QgsRendererCategory(nama, simbol, nama))
petak.setRenderer(QgsCategorizedSymbolRenderer("Jenis", kategori))

# Sungai: garis biru; Jalan: garis cokelat putus-putus; Titik panas: lingkaran merah
sungai.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple(
    {"line_color": "#2B6CB0", "line_width": "0.8"})))
jalan.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple(
    {"line_color": "#5A4632", "line_width": "0.5", "line_style": "dash"})))
titik.setRenderer(QgsSingleSymbolRenderer(QgsMarkerSymbol.createSimple(
    {"name": "circle", "color": "#E03C31", "outline_color": "#FFFFFF", "size": "2.6"})))

for layer in (petak, sungai, jalan, titik):
    layer.triggerRepaint()
print("Kategori dipasang:", [k.label() for k in petak.renderer().categories()])
print("Layer di proyek:", [l.name() for l in proyek.mapLayers().values()])

Penjelasan baris demi baris. Nomor baris mengikuti berkas di atas.

BarisTulisanArtinya
1-2# Skrip 4.1 ...Komentar. Python melewatinya. Isinya menyebut bahwa skrip memuat empat layer: petak, sungai, jalan, dan titik panas.
3-5from qgis.core import (...)Panggil sembilan perkakas QGIS. Tanda kurung membolehkan daftar nama disambung ke beberapa baris.
7FOLDER = r"D:/KPH_Contoh"Isi kotak FOLDER dengan lokasi folder kerja. Huruf r membuat teks dibaca apa adanya. Bagian ini yang Anda ganti.
8proyek = QgsProject.instance()Ambil proyek QGIS yang sedang terbuka, lalu simpan di kotak proyek.
9proyek.setCrs(...)Atur sistem koordinat proyek ke EPSG:32749, yaitu UTM zona 49 Selatan dengan satuan meter.
11-15def muat(berkas, lapisan, nama):Buat fungsi bernama muat, yaitu resep yang bisa dipakai berkali-kali dengan tiga isian (dibahas di Bab 7). Isinya membuat layer, memeriksanya, menambahkannya ke proyek, lalu mengembalikannya.
12layer = QgsVectorLayer("%s/%s...Susun alamat data dari tiga isian: folder, berkas, lalu garis tegak dan nama layer di dalam berkas. Tanda %s diganti isian berurutan oleh tanda % di ujung (format teks, Bab 9). Isian terakhir, "ogr", adalah pembaca data vektor.
13assert layer.isValid(), "Layer tidak terbaca: " + berkasPemeriksa (assert, Bab 10). Fungsi isValid() bernilai True bila data berhasil dibaca. Bila salah, skrip berhenti dengan AssertionError dan pesan Layer tidak terbaca: diikuti nama berkasnya. Tanpa baris ini, jalur yang salah menghasilkan layer kosong tanpa peringatan.
14proyek.addMapLayer(layer)Tambahkan layer ke proyek. Layer yang ditambah belakangan berada di atas.
17-20petak = muat(...) dan tiga baris di bawahnyaPanggil muat empat kali. Hasilnya masuk ke kotak petak, sungai, jalan, dan titik.
23-24warna = {...}Kamus (Bab 5): pasangan nama jenis dan kode warna heksadesimal. Contoh "#8C6D31" adalah cokelat. Nama jenis harus persis sama dengan isi kolom Jenis.
25kategori = []Siapkan daftar kosong untuk menampung kategori.
26for nama, hexa in warna.items():Ulangi untuk tiap pasangan di kamus. Di tiap putaran, nama berisi nama jenis dan hexa berisi kode warnanya.
27simbol = QgsFillSymbol.createSimple({...})Buat simbol isian poligon. Kamus kecil di dalamnya mengatur warna isi, warna garis tepi, dan tebal garis tepi (dalam milimeter).
28kategori.append(QgsRendererCategory(...))Buat satu kategori dari tiga hal: nilai yang dicari, simbolnya, dan tulisan di legenda. Lalu tambahkan ke daftar.
29petak.setRenderer(QgsCategorizedSymbolRenderer("Jenis", kategori))Pasang penyaji berkategori pada petak. Penyaji melihat kolom Jenis, lalu memilih simbol menurut isinya.
32-33sungai.setRenderer(...)Beri sungai satu simbol garis biru setebal 0,8 mm. QgsSingleSymbolRenderer berarti satu simbol untuk semua fitur.
34-35jalan.setRenderer(...)Beri jalan garis cokelat putus-putus. Pengaturan "line_style": "dash" membuatnya putus-putus.
36-37titik.setRenderer(...)Beri titik panas simbol lingkaran merah dengan garis tepi putih, ukuran 2,6.
39-40for layer in (...): layer.triggerRepaint()Ulangi untuk keempat layer. Minta QGIS menggambar ulang agar warna baru terlihat.
41print("Kategori dipasang:", [...])Tampilkan daftar tulisan kategori. Tanda kurung siku dengan for di dalamnya adalah list comprehension (Bab 6).
42print("Layer di proyek:", [...])Tampilkan nama semua layer di proyek.

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

HASIL
Kategori dipasang: ['Jati', 'Mahoni', 'Sengon', 'Akasia', 'Agroforestri']
Layer di proyek: ['Hotspot_Nasional', 'Jalan', 'Petak_KPH', 'Sungai']

Urutan nama pada baris kedua mengikuti catatan internal QGIS, bukan urutan di panel Layers. Di peta (saya merendernya sendiri untuk memastikan), petak tampil dalam lima warna menurut jenis, sungai berupa garis biru, jalan putus-putus cokelat, dan 12 titik merah.

Bagian yang boleh diubah.

  • Baris 7, lokasi data. Ganti jalur dengan folder kerja Anda.
  • Baris 23-24, warna. Ganti kode heksadesimal untuk mengubah warna jenis tertentu. Nama jenisnya tidak boleh diubah.
  • Baris 27, outline_width. Angka "0.3" adalah tebal garis tepi petak. Ganti menjadi "0.1" untuk garis lebih tipis.
  • Baris 33 dan 35, line_width. Ganti angkanya untuk mengatur tebal sungai dan jalan.
  • Baris 35, line_style. Ganti "dash" menjadi "solid" untuk garis penuh.
  • Baris 37, size. Angka "2.6" adalah ukuran titik panas.
  • Baris 29, "Jenis". Ganti dengan kolom teks lain untuk mewarnai menurut kolom itu.

Jebakan.

  • Jalur salah menghentikan skrip. Bila FOLDER menunjuk folder yang tidak ada, baris 13 menghentikan skrip dengan AssertionError: Layer tidak terbaca: diikuti nama berkas pertama yang gagal. Pesan ini menolong, karena Anda tahu penyebabnya sebelum melihat peta kosong. Tanpa baris 13, skrip jalan terus, keempat layer kosong, dan peta tidak menampilkan apa pun. Bila pesan itu muncul, periksa baris 7 dan nama berkas.
  • Jenis yang tidak ada di kamus. Misalnya Anda menghapus "Akasia" dari kamus warna. Petak berjenis Akasia lalu tidak punya kategori. Uji menunjukkan categoryIndexForValue("Akasia") bernilai -1, artinya tidak ada kategori yang cocok. Petak itu tidak tergambar di peta: pada hasil render saya, petak B03 (Akasia) tampil putih tanpa isian, dan legenda tidak memuat Akasia.
  • Komentar bisa tertinggal dari isi. Python tidak memeriksa komentar. Bila Anda menambah layer, ubah juga komentar di baris 1. Saat membaca skrip orang lain, percayai kodenya, bukan komentarnya.
  • Skrip berikutnya menimpa warna. Skrip 17.2 memasang penyaji baru pada layer Petak_KPH. Warna per jenis dari skrip ini lalu hilang, diganti warna menurut volume.

Latihan variasi.

  1. Ganti warna Jati menjadi hijau tua #2E7D32.
  2. Buat sungai lebih tebal, yaitu 1,2.

Contoh jawaban, hanya bagian yang berubah:

PYTHON
warna = {"Jati": "#2E7D32", "Mahoni": "#B5523B", "Sengon": "#7FB069",
         "Akasia": "#D9A400", "Agroforestri": "#4F9DA6"}
PYTHON
sungai.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple(
    {"line_color": "#2B6CB0", "line_width": "1.2"})))

Saya menambah satu baris print untuk memeriksa. Hasilnya Warna Jati: #2e7d32 | tebal sungai: 1.2.

Skrip 17.2: Klasifikasi angka menjadi kelas warna #

Berkas skrip_4_2_klasifikasi.py. Di dalam berkasnya tertulis "Skrip 4.2".

Tujuan. Mewarnai petak menurut Volume_m3ha dalam empat kelas, membandingkan tiga metode pembagian kelas, dan menampilkan petak yang belum diukur sebagai layer abu-abu tersendiri.

Bahan. Jalankan Skrip 17.1 lebih dulu, di proyek yang sama, agar layer Petak_KPH sudah ada. Folder kerja juga memuat Petak_KPH.gpkg, karena baris 43 membukanya sekali lagi. Dari 16 petak, 15 punya volume dan satu petak, G08, kosong.

Angka seperti volume tidak cocok diwarnai satu warna per nilai, karena hampir semua nilainya berbeda. Solusinya membagi angka menjadi kelas (rentang), lalu memberi satu warna untuk tiap kelas. Penyaji untuk ini bernama Graduated.

Bayangkan Anda membagi murid ke dalam kelas. Ada tiga cara membaginya, dan hasilnya berbeda.

  • Equal Interval: tiap kelas selebar sama. Rentang nilai dipotong rata, tanpa melihat banyaknya petak.
  • Quantile (di QGIS bernama Equal Count): tiap kelas berisi petak yang jumlahnya hampir sama.
  • Natural Breaks (juga disebut Jenks): batas kelas dipasang di celah besar antar kelompok nilai.
Ilustrasi 17.4: Tiga metode membagi 15 nilai volume
Skema tiga baris titik yang sama, yaitu 15 nilai volume kayu, dengan garis batas kelas berbeda untuk Equal Interval, Quantile, dan Natural Breaks

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

Ilustrasi 17.5: Alur skrip 17.2
Skema delapan tahap skrip: ambil layer, cari petak kosong, saring sementara, fungsi pasang, bandingkan tiga metode, pasang Quantile dan lepas saringan, layer Belum diukur, sisipkan di atas Petak_KPH

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.2: simbologi Graduated untuk Volume_m3ha, membandingkan metode klasifikasi,
# dan menangani petak yang nilainya kosong (NULL)
from qgis.core import (QgsProject, QgsStyle, QgsGraduatedSymbolRenderer, QgsClassificationEqualInterval,
                       QgsClassificationQuantile, QgsClassificationJenks, QgsFillSymbol, QgsVectorLayer,
                       QgsSingleSymbolRenderer, QgsSymbolLayerUtils)

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda

petak = QgsProject.instance().mapLayersByName("Petak_KPH")[0]

# 1. Periksa nilai kosong. Metode Quantile dan Natural Breaks membacanya sebagai nol.
kosong = [f["ID_Petak"] for f in petak.getFeatures() if f["Volume_m3ha"] is None]
print("Petak tanpa nilai:", kosong)

# 2. Saring sementara: hanya petak yang terisi ikut dihitung
petak.setSubsetString('"Volume_m3ha" IS NOT NULL')

def pasang(kelas_metode, jumlah=4):
    metode = kelas_metode()     # objek metode baru tiap kali dipakai
    rend = QgsGraduatedSymbolRenderer("Volume_m3ha")
    rend.setSourceSymbol(QgsFillSymbol.createSimple({"outline_color": "#404040", "outline_width": "0.3"}))
    rend.setClassificationMethod(metode)
    rend.updateClasses(petak, jumlah)
    rend.updateColorRamp(QgsStyle.defaultStyle().colorRamp("YlGn"))   # skala kuning ke hijau, objek baru tiap kali
    return rend

for kelas_metode in (QgsClassificationEqualInterval, QgsClassificationQuantile, QgsClassificationJenks):
    rend = pasang(kelas_metode)
    print(rend.classificationMethod().name())
    for i, r in enumerate(rend.ranges()):
        # nilai tepat di batas masuk kelas bawahnya; kelas pertama memuat nilai terkecil
        n = sum(1 for f in petak.getFeatures()
                if (r.lowerValue() < f["Volume_m3ha"] <= r.upperValue()) or (i == 0 and f["Volume_m3ha"] == r.lowerValue()))
        print("   %7.1f - %7.1f   %d petak" % (r.lowerValue(), r.upperValue(), n))

# 3. Pakai Quantile (4 kelas) pada layer, lalu lepas saringan
rend = pasang(QgsClassificationQuantile)
petak.setRenderer(rend)
petak.setSubsetString("")
petak.triggerRepaint()

# 4. Petak yang belum diukur ditampilkan sebagai layer tersendiri berwarna abu-abu
belum = QgsVectorLayer(FOLDER + "/Petak_KPH.gpkg|layername=Petak_KPH", "Belum diukur", "ogr")
belum.setSubsetString('"Volume_m3ha" IS NULL')
belum.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
    {"color": "#D0D0D0", "outline_color": "#404040", "outline_width": "0.3", "style": "b_diagonal"})))
proyek = QgsProject.instance()
proyek.addMapLayer(belum, False)              # False: belum dimasukkan ke panel Layers
akar = proyek.layerTreeRoot()
posisi = akar.children().index(akar.findLayer(petak.id()))
akar.insertLayer(posisi, belum)               # tepat di atas Petak_KPH
print("Layer Belum diukur:", belum.featureCount(), "petak")

Penjelasan baris demi baris. Bagian yang sama dengan Skrip 17.1 tidak dijelaskan ulang. Baris 3-5 dan baris 7 mengikuti pola yang sama, hanya nama perkakasnya berbeda.

BarisTulisanArtinya
3-5from qgis.core import (...)Panggil sepuluh perkakas. Satu di antaranya, QgsSymbolLayerUtils, tidak dipakai. Impor yang tersisa tidak berpengaruh.
9petak = QgsProject.instance().mapLayersByName("Petak_KPH")[0]Cari layer di proyek menurut namanya. Hasilnya berupa daftar, jadi [0] mengambil yang pertama.
12kosong = [f["ID_Petak"] for f in ... if f["Volume_m3ha"] is None]Kumpulkan kode petak yang volumenya kosong. Nilai kosong di Python bernama None.
13print("Petak tanpa nilai:", kosong)Tampilkan petak yang kosong.
16petak.setSubsetString('"Volume_m3ha" IS NOT NULL')Pasang saringan sementara: layer hanya "melihat" petak yang volumenya terisi. Teks luar memakai kutip tunggal supaya nama kolom di dalamnya boleh memakai kutip ganda.
18def pasang(kelas_metode, jumlah=4):Fungsi dengan dua isian. Isian jumlah punya nilai bawaan 4, jadi boleh tidak diisi (Bab 7).
19metode = kelas_metode()kelas_metode berisi satu "cetakan" metode. Tanda kurung membuat satu objek baru dari cetakan itu. Komentar mengingatkan: objek baru tiap kali.
20rend = QgsGraduatedSymbolRenderer("Volume_m3ha")Buat penyaji bertingkat yang membaca kolom Volume_m3ha.
21rend.setSourceSymbol(...)Tetapkan simbol dasar: hanya garis tepi abu tua. Warna isi akan datang dari skala warna.
22rend.setClassificationMethod(metode)Pilih metode pembagian kelas.
23rend.updateClasses(petak, jumlah)Hitung batas kelas dari data layer, sebanyak jumlah kelas.
24rend.updateColorRamp(...colorRamp("YlGn"))Pasang skala warna YlGn, dari kuning ke hijau. Objek skala warna dibuat baru tiap kali.
25return rendKembalikan penyaji yang sudah jadi.
27for kelas_metode in (A, B, C):Ulangi tiga kali, sekali untuk tiap metode. Nama metode disimpan di dalam tanda kurung seperti nilai biasa.
28-29rend = pasang(kelas_metode), print(...)Buat penyaji dengan metode itu, lalu tampilkan nama metodenya.
30for i, r in enumerate(rend.ranges()):Ulangi untuk tiap kelas. enumerate menambahkan nomor urut i dari 0 (Bab 6).
32-33n = sum(1 for f in ... if (...) or (...))Hitung petak di kelas ini. Tiap petak yang memenuhi syarat menyumbang angka 1. Syarat pertama, batas_bawah < nilai <= batas_atas, adalah perbandingan berantai. Syarat kedua membuat nilai terkecil ikut masuk kelas pertama.
34print(" %7.1f - %7.1f %d petak" % (...))Cetak batas bawah, batas atas, dan jumlah petak. %7.1f berarti angka desimal, satu angka di belakang koma, lebar 7 karakter.
37-38rend = pasang(QgsClassificationQuantile), petak.setRenderer(rend)Pilih Quantile, empat kelas, lalu pasang di layer.
39-40petak.setSubsetString(""), petak.triggerRepaint()Lepas saringan, supaya petak kosong muncul kembali di peta. Lalu gambar ulang.
43belum = QgsVectorLayer(FOLDER + "/Petak_KPH.gpkg...Buka data yang sama sekali lagi sebagai layer baru bernama Belum diukur.
44belum.setSubsetString('"Volume_m3ha" IS NULL')Saringan kebalikannya: hanya petak yang volumenya kosong.
45-46belum.setRenderer(...)Beri simbol isian abu-abu dengan pola garis miring (b_diagonal).
47proyek = QgsProject.instance()Ambil proyek dan simpan di kotak proyek. Baris 9 dan 38 tadi memanggilnya langsung.
48proyek.addMapLayer(belum, False)Daftarkan layer ke proyek. Isian False berarti belum dimasukkan ke panel Layers.
49akar = proyek.layerTreeRoot()Ambil "akar pohon" panel Layers.
50posisi = akar.children().index(akar.findLayer(petak.id()))Cari nomor urut Petak_KPH di panel Layers.
51akar.insertLayer(posisi, belum)Sisipkan Belum diukur pada nomor urut itu, sehingga tepat di atas Petak_KPH.
52print("Layer Belum diukur:", ...)Tampilkan jumlah petak di layer itu.

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

HASIL
Petak tanpa nilai: ['G08']
Equal Interval
      45.5 -    97.7   8 petak
      97.7 -   149.8   0 petak
     149.8 -   202.0   4 petak
     202.0 -   254.1   3 petak
Equal Count (Quantile)
      45.5 -    67.3   4 petak
      67.3 -    89.1   4 petak
      89.1 -   197.1   3 petak
     197.1 -   254.1   4 petak
Natural Breaks (Jenks)
      45.5 -    60.4   4 petak
      60.4 -    89.1   4 petak
      89.1 -   161.4   2 petak
     161.4 -   254.1   5 petak
Layer Belum diukur: 1 petak

Dari hasil ini Anda tahu tiga hal. Pertama, G08 kosong. Kedua, Equal Interval menyisakan satu kelas tanpa petak. Ketiga, setiap metode menghasilkan 15 petak bila dijumlahkan.

Satu aturan batas kelas perlu Anda tahu. Nilai yang tepat berada di batas masuk ke kelas di bawahnya. Contohnya A02 bervolume 89,1. Pada metode Quantile, nilai itu masuk kelas 67,3 sampai 89,1, bukan kelas berikutnya.

Bagian yang boleh diubah.

  • Baris 18, jumlah=4. Ganti angka bawaan untuk mengubah jumlah kelas. Anda juga boleh menulis pasang(kelas_metode, 5).
  • Baris 24, "YlGn". Ganti dengan nama skala warna lain, misalnya "Blues". Uji: "Blues" berjalan baik dan menghasilkan warna kelas dari #f7fbff sampai #08306b.
  • Baris 37, metode. Ganti QgsClassificationQuantile dengan QgsClassificationJenks atau QgsClassificationEqualInterval.
  • Baris 12, 16, 20, 33, dan 44, nama kolom. Ganti Volume_m3ha dengan kolom angka lain. Ganti di semua baris itu.
  • Baris 45, "color": "#D0D0D0". Ganti untuk mengubah warna abu-abu petak kosong.

Jebakan.

  • Skrip 17.1 belum dijalankan. Layar menampilkan IndexError: list index out of range pada baris 9. Artinya daftar hasil pencarian kosong, karena layer Petak_KPH tidak ada di proyek. Jalankan Skrip 17.1 dulu.
  • Nilai kosong dibaca sebagai nol. Baris 11 menyatakan bahwa Quantile dan Natural Breaks membaca nilai kosong sebagai nol. Saya mengujinya dengan membuang saringan di baris 16. Hasilnya, batas bawah kelas pertama Quantile menjadi 0,0 dan Natural Breaks menghasilkan kelas 0.0 - 0.0. Karena itu skrip menyaring dulu.
  • Salah ketik nama kolom di baris 12. Menulis f["Volumee"] menghasilkan KeyError: 'Volumee'. Salin nama kolom dari tabel atribut.
  • Salah ketik nama kolom di baris 16. Menulis "Volumee" IS NOT NULL tidak menghasilkan galat di baris itu. Saringan tidak bekerja, dan galat muncul kemudian di baris 33: TypeError: '<' not supported between instances of 'float' and 'NoneType'. Petak G08 ikut terhitung, dan angka kosongnya tidak bisa dibandingkan. Pelajarannya: galat bisa muncul jauh dari sumber salahnya.
  • Petak kosong tidak digambar. Saya merender peta tanpa layer Belum diukur. Hasilnya, kotak petak G08 kosong (putih), karena penyaji bertingkat tidak punya kelas untuk nilai kosong. Itulah alasan skrip menambah layer abu-abu tersendiri di baris 43-52. Jangan hapus layer itu bila petak kosong harus tetap terlihat.
  • Dijalankan ulang. Setiap kali skrip ini dijalankan, satu layer Belum diukur baru ditambahkan lagi (uji: dua kali jalan memberi dua layer bernama Belum diukur). Hapus layer lama lebih dulu, atau mulai dari proyek baru.
  • Menimpa warna Skrip 17.1. Setelah skrip ini jalan, petak berwarna menurut volume, bukan menurut jenis. Itu wajar. Untuk kembali, jalankan ulang bagian penyaji di Skrip 17.1.
  • Jangan membagi satu objek ke banyak penyaji. Catatan Buku B4 menyebut bahwa memakai ulang satu objek skala warna pada beberapa penyaji membuat QGIS berhenti mendadak. Karena itu baris 19 dan 24 membuat objek baru tiap kali. Hal ini tidak saya ulangi di sini. [CEK]

Latihan variasi.

  1. Pasang metode Natural Breaks dengan lima kelas pada layer, bukan Quantile empat kelas.
  2. Cetak batas kelas hasilnya.

Contoh jawaban. Ganti baris 37, lalu tambahkan satu baris print:

PYTHON
rend = pasang(QgsClassificationJenks, 5)
print("Pilihan latihan:", [(round(r.lowerValue(), 1), round(r.upperValue(), 1)) for r in rend.ranges()])

Layar menampilkan Pilihan latihan: [(45.5, 60.4), (60.4, 89.1), (89.1, 161.4), (161.4, 226.4), (226.4, 254.1)].

Skrip 17.3: Warna untuk peta tinggi dan bayangan bukit #

Berkas skrip_4_2b_raster.py. Di dalam berkasnya tertulis "Skrip 4.2b". Skrip ini sedikit keluar dari alur, karena menangani raster, bukan vektor.

Tujuan. Memberi skala warna Viridis lima kelas pada peta tinggi (DTM), lalu membuat layer salinan yang menampilkan bayangan bukit.

Bahan. Berkas DTM_Drone.tif di folder kerja. Skrip ini berdiri sendiri, karena memuat raster sendiri.

Raster adalah gambar dari kotak-kotak kecil (piksel), dan tiap kotak menyimpan satu angka. Di sini angkanya tinggi tempat dalam meter, dari 15 sampai 25 meter. Raster dibahas lebih lengkap di Bab 16.

Tiga istilah baru:

  • Pseudocolor: tiap rentang angka diberi satu warna.
  • Hillshade: bayangan bukit, yang memberi kesan relief dari arah cahaya.
  • Band: satu lapis angka dalam raster. DTM hanya punya satu.
Ilustrasi 17.6: Alur skrip 17.3
Skema lima tahap skrip: muat DTM dan baca statistik, siapkan skala warna Viridis, pasang pseudocolor, salinan layer bayangan bukit, tambah ke proyek

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.2b: simbologi raster. Peta tinggi (DTM) diberi skala warna, lalu dibuat versi bayangan bukit
from qgis.core import (QgsProject, QgsRasterLayer, QgsStyle, QgsColorRampShader,
                       QgsRasterShader, QgsSingleBandPseudoColorRenderer, QgsHillshadeRenderer, Qgis)

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda

dtm = QgsRasterLayer(FOLDER + "/DTM_Drone.tif", "DTM_Drone")
assert dtm.isValid(), "Raster tidak terbaca: " + FOLDER + "/DTM_Drone.tif"
statistik = dtm.dataProvider().bandStatistics(1)
print("Tinggi terendah %.1f m, tertinggi %.1f m" % (statistik.minimumValue, statistik.maximumValue))

# 1. Pseudocolor: tiap rentang tinggi diberi warna dari skala Viridis
fungsi = QgsColorRampShader(statistik.minimumValue, statistik.maximumValue)
fungsi.setSourceColorRamp(QgsStyle.defaultStyle().colorRamp("Viridis"))
fungsi.setClassificationMode(Qgis.ShaderClassificationMethod.EqualInterval)
fungsi.classifyColorRamp(5)                       # 5 kelas warna sama lebar
bayangan_warna = QgsRasterShader()
bayangan_warna.setRasterShaderFunction(fungsi)
dtm.setRenderer(QgsSingleBandPseudoColorRenderer(dtm.dataProvider(), 1, bayangan_warna))
print("Kelas warna:", [(round(i.value, 1), i.color.name()) for i in fungsi.colorRampItemList()])

# 2. Hillshade (bayangan bukit) pada salinan layer: arah cahaya 315 derajat, tinggi matahari 45 derajat
bukit = QgsRasterLayer(FOLDER + "/DTM_Drone.tif", "DTM_Bayangan")
bukit.setRenderer(QgsHillshadeRenderer(bukit.dataProvider(), 1, 315, 45))
print("Renderer bukit:", bukit.renderer().type())

QgsProject.instance().addMapLayer(dtm)
QgsProject.instance().addMapLayer(bukit)

Penjelasan baris demi baris.

BarisTulisanArtinya
2-3from qgis.core import (...)Panggil delapan perkakas, termasuk Qgis yang memuat daftar pilihan bernama.
5FOLDER = ...Lokasi folder kerja. Ganti dengan jalur Anda.
7dtm = QgsRasterLayer(FOLDER + "/DTM_Drone.tif", "DTM_Drone")Buka raster sebagai layer. Hanya dua isian: alamat dan nama. Raster tidak butuh isian "ogr".
8assert dtm.isValid(), "Raster tidak terbaca: " + ...Pemeriksa (Bab 10). Bila raster tidak terbaca, skrip berhenti dengan AssertionError dan pesan Raster tidak terbaca: diikuti jalur berkasnya.
9statistik = dtm.dataProvider().bandStatistics(1)Minta penyedia data layer menghitung statistik band nomor 1: terendah, tertinggi, rata-rata.
10print("Tinggi terendah %.1f m, ..." % (...))Tampilkan tinggi terendah dan tertinggi, satu desimal.
13fungsi = QgsColorRampShader(min, max)Buat pengatur warna untuk rentang dari tinggi terendah sampai tertinggi.
14fungsi.setSourceColorRamp(...colorRamp("Viridis"))Pakai skala warna Viridis. Skala ini bergerak dari ungu tua ke kuning.
15fungsi.setClassificationMode(...EqualInterval)Pembagian kelas dengan selebar sama. Pilihan namanya diambil dari perkakas Qgis.
16fungsi.classifyColorRamp(5)Bagi skala warna menjadi lima kelas.
17-18bayangan_warna = QgsRasterShader() lalu setRasterShaderFunction(fungsi)Bungkus pengatur warna ke dalam "pewarna raster". Nama kotak bayangan_warna membingungkan, karena isinya pewarna biasa, bukan bayangan bukit.
19dtm.setRenderer(QgsSingleBandPseudoColorRenderer(...))Pasang penyaji pseudocolor pada layer: band 1, diwarnai oleh pewarna tadi.
20print("Kelas warna:", [(round(i.value, 1), i.color.name()) ...])Tampilkan tiap kelas: nilai batasnya dan kode warnanya.
23bukit = QgsRasterLayer(...)Buka berkas yang sama sekali lagi sebagai layer baru, DTM_Bayangan.
24bukit.setRenderer(QgsHillshadeRenderer(provider, 1, 315, 45))Pasang penyaji bayangan bukit. Angka 315 adalah arah cahaya dalam derajat (dari barat laut). Angka 45 adalah tinggi matahari dalam derajat.
25print("Renderer bukit:", bukit.renderer().type())Tampilkan jenis penyaji, untuk memastikan terpasang.
27-28QgsProject.instance().addMapLayer(...)Tambahkan kedua layer ke proyek. bukit ditambah belakangan, jadi berada di atas dtm.

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

HASIL
Tinggi terendah 15.0 m, tertinggi 25.0 m
Kelas warna: [(15.0, '#440154'), (17.5, '#3b528b'), (20.0, '#21908d'), (22.5, '#5dc963'), (25.0, '#fde725')]
Renderer bukit: hillshade

Lima nilai batas tampil: 15,0, 17,5, 20,0, 22,5, dan 25,0 meter. Warna pertama #440154 adalah ungu tua, dan warna terakhir #fde725 adalah kuning. Data DTM latihan memang buatan, jadi rentang tingginya sempit.

Bagian yang boleh diubah.

  • Baris 7 dan 23, nama berkas. Ganti DTM_Drone.tif dengan raster lain. Ganti di kedua baris.
  • Baris 14, nama skala warna. Contoh: "Magma".
  • Baris 16, jumlah kelas. Angka 5 boleh diganti.
  • Baris 24, 315 dan 45. Ubah arah dan tinggi cahaya. Contoh 225, 30 memberi bayangan lebih panjang dari arah barat daya.

Jebakan.

  • Berkas tidak ditemukan menghentikan skrip. Bila jalur salah, baris 8 menghentikan skrip dengan AssertionError: Raster tidak terbaca: diikuti jalur lengkapnya. Tanpa baris itu, skrip tetap berjalan dan mencetak tinggi terendah sekitar 1,8 kali 10 pangkat 308, tinggi tertinggi minus angka yang sama, lalu Kelas warna: []. Angka raksasa itu tanda raster tidak terbaca. Bila pesan muncul, periksa jalur di baris 5.
  • Nama skala warna salah. Menulis "Viridiss" tidak menimbulkan galat. QGIS diam-diam memakai skala lain, dari biru ke merah. Uji: warna pertama #0000ff, warna terakhir #cc0033. Periksa Kelas warna pada hasil.
  • Bayangan bukit menutup peta. Layer DTM_Bayangan berada di atas semua layer lain. Bila skrip ini dijalankan di proyek yang sama sebelum Skrip 17.5, layout hasil ekspor tertutup bidang abu-abu, dan petak tidak tampak. Hal ini saya lihat langsung dari hasil ekspor. Jalankan skrip ini di proyek terpisah, atau setelah layout diekspor.
  • Dua layer dari satu berkas. Ini sengaja. Satu layer tidak bisa memakai dua penyaji sekaligus, jadi skrip membuat salinan.

Latihan variasi. Ubah skrip menjadi tiga kelas, skala warna Magma, dan cahaya dari arah 225 derajat dengan tinggi matahari 30 derajat.

Contoh jawaban, hanya bagian yang berubah:

PYTHONUnduh berkas
fungsi.setSourceColorRamp(QgsStyle.defaultStyle().colorRamp("Magma"))
fungsi.classifyColorRamp(3)
bukit.setRenderer(QgsHillshadeRenderer(bukit.dataProvider(), 1, 225, 30))

Layar menampilkan Kelas warna: [(15.0, '#000004'), (20.0, '#b6377a'), (25.0, '#fcfdbf')] dan Renderer bukit: hillshade.

Skrip 17.4: Label berisi kode petak dan volume #

Berkas skrip_4_3_label.py. Di dalam berkasnya tertulis "Skrip 4.3".

Tujuan. Memberi label pada tiap petak berisi kode dan volumenya, dengan garis tepi putih agar terbaca, lalu menguji hasil ekspresi pada dua petak.

Bahan. Jalankan Skrip 17.1 lebih dulu, agar layer Petak_KPH sudah ada di proyek. Skrip ini tidak membaca berkas lain.

Label adalah tulisan di atas peta. Isinya bisa kolom biasa, bisa juga ekspresi (rumus kecil yang menyusun teks dari beberapa kolom). Garis tepi putih di sekeliling huruf disebut halo. Halo membuat huruf tetap terbaca di atas warna gelap.

Ilustrasi 17.7: Alur skrip 17.4
Skema enam tahap skrip: ambil layer, susun ekspresi, pengaturan label, format teks dan halo, pasang label, uji ekspresi

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.3: label petak berisi ID dan volume, dengan garis tepi putih agar terbaca
from qgis.core import (QgsProject, QgsPalLayerSettings, QgsTextFormat, QgsTextBufferSettings,
                       QgsVectorLayerSimpleLabeling, QgsExpression, QgsExpressionContext,
                       QgsExpressionContextUtils)
from qgis.PyQt.QtGui import QColor, QFont

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda

petak = QgsProject.instance().mapLayersByName("Petak_KPH")[0]

ekspresi = ('"ID_Petak" || \'\n\' || '
            'coalesce(to_string(round("Volume_m3ha")) || \' m3/ha\', \'belum diukur\')')

pengaturan = QgsPalLayerSettings()
pengaturan.fieldName = ekspresi
pengaturan.isExpression = True

teks = QgsTextFormat()
teks.setFont(QFont("Arial"))
teks.setSize(8)
teks.setColor(QColor("#202020"))
garis = QgsTextBufferSettings()
garis.setEnabled(True)
garis.setSize(0.8)
garis.setColor(QColor("white"))
teks.setBuffer(garis)
pengaturan.setFormat(teks)

petak.setLabeling(QgsVectorLayerSimpleLabeling(pengaturan))
petak.setLabelsEnabled(True)
petak.triggerRepaint()

# periksa hasil ekspresi pada dua petak
konteks = QgsExpressionContext()
konteks.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(petak))
for f in petak.getFeatures():
    if f["ID_Petak"] in ("A01", "G08"):
        konteks.setFeature(f)
        print(f["ID_Petak"], "->", repr(QgsExpression(ekspresi).evaluate(konteks)))

Penjelasan baris demi baris. Garis tegak ganda || di dalam ekspresi berfungsi menyambung teks. Karena tanda garis tegak mengganggu tabel, sel di bawah menyebutnya "garis tegak ganda".

BarisTulisanArtinya
2-5from qgis.core import (...) dan from qgis.PyQt.QtGui import QColor, QFontPanggil perkakas label dari QGIS, lalu perkakas warna dan huruf dari PyQt (perangkat jendela yang dipakai QGIS).
7FOLDER = ...Didefinisikan, tetapi tidak dipakai di skrip ini. Tidak berbahaya.
9petak = ...mapLayersByName("Petak_KPH")[0]Cari layer Petak_KPH di proyek.
11-12ekspresi = (... '...' ...)Rumus label, ditulis sebagai teks dalam dua potongan yang disambung otomatis oleh tanda kurung. Hasilnya, dalam bahasa QGIS: kode petak, lalu baris baru, lalu volume yang dibulatkan diikuti m3/ha. Bila volumenya kosong, tampil belum diukur. Penjelasan bagian-bagiannya ada di bawah tabel.
14pengaturan = QgsPalLayerSettings()Buat kotak pengaturan label yang masih kosong.
15-16pengaturan.fieldName = ekspresi, pengaturan.isExpression = TrueIsi pengaturan: teks yang dilabel adalah ekspresi, dan teks itu harus dibaca sebagai rumus, bukan nama kolom. Titik di tengah artinya "milik pengaturan ini" (Bab 11).
18teks = QgsTextFormat()Buat kotak format teks yang kosong.
19-21teks.setFont(QFont("Arial")), setSize(8), setColor(QColor("#202020"))Atur jenis huruf Arial, ukuran 8, dan warna abu sangat tua.
22-26garis = QgsTextBufferSettings() dan baris di bawahnyaBuat pengaturan halo. Nyalakan, atur tebal 0,8, warna putih, lalu pasang ke format teks.
27pengaturan.setFormat(teks)Pasang format teks ke pengaturan label.
29petak.setLabeling(QgsVectorLayerSimpleLabeling(pengaturan))Pasang pengaturan sebagai label layer. "Simple" berarti satu pengaturan untuk semua fitur.
30-31petak.setLabelsEnabled(True), petak.triggerRepaint()Nyalakan label, lalu gambar ulang.
34-35konteks = QgsExpressionContext() dan appendScopes(...)Siapkan "ruang pandang" untuk menghitung ekspresi: variabel global, proyek, dan layer ini.
36-37for f in petak.getFeatures(): if f["ID_Petak"] in ("A01", "G08"):Ulangi untuk tiap petak, tetapi hanya proses petak A01 dan G08.
38konteks.setFeature(f)Beritahu ruang pandang: petak ini yang sedang dihitung.
39print(..., repr(QgsExpression(ekspresi).evaluate(konteks)))Hitung ekspresi pada petak itu, lalu cetak dengan repr. Fungsi repr menampilkan tanda khusus apa adanya, jadi baris baru tampil sebagai \n.

Membaca ekspresi di baris 11-12. Rumus label itu terdiri dari empat bagian. Garis tegak ganda menyambung dua teks. round membulatkan angka ke bilangan bulat. to_string mengubah angka menjadi teks, supaya bisa disambung.

coalesce mengambil isian pertama yang tidak kosong, dan bila semuanya kosong dipakai isian terakhir. Karena angka kosong disambung dengan teks hasilnya kosong, petak G08 jatuh ke teks belum diukur.

Dua tanda di baris 11-12 sering membingungkan. Pertama, \' adalah kutip tunggal yang ditulis di dalam teks berkutip tunggal. Garis miring terbalik di depannya berarti "ini bagian teks, bukan penutup".

Kedua, \n adalah baris baru. Karena itu label tampil dalam dua baris.

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

HASIL
A01 -> 'A01\n46 m3/ha'
G08 -> 'G08\nbelum diukur'

Petak A01 bervolume 45,5, dan hasilnya dibulatkan menjadi 46. Di peta, tiap petak yang digambar punya label dua baris. Label G08 berbunyi "belum diukur" bila petak itu digambar (lihat Jebakan).

Bagian yang boleh diubah.

  • Baris 11-12, ekspresi. Ganti untuk menyusun label lain. Nama kolom ditulis dengan kutip ganda, dan teks biasa dengan kutip tunggal.
  • Baris 19, jenis huruf. Ganti "Arial" dengan huruf yang terpasang di komputer Anda.
  • Baris 20, ukuran. Ganti angka 8.
  • Baris 21, warna. Ganti kode warna huruf.
  • Baris 24, tebal halo. Ganti 0,8. Nilai 0 mematikan efek.
  • Baris 37, ID petak. Ganti "A01", "G08" dengan petak yang ingin Anda periksa.

Jebakan.

  • Lupa garis miring terbalik. Menulis ekspresi baru dengan kutip tunggal di dalam kutip tunggal, misalnya '"ID_Petak" || ' - ' || "Jenis"', menghasilkan TypeError: unsupported operand type(s) for -: 'str' and 'str'. Python mengira tanda kutip di tengah menutup teks. Perbaikan: tulis \' - \'.
  • Skrip 17.1 belum dijalankan. Layar menampilkan IndexError: list index out of range pada baris 9.
  • Nama layer diganti di panel Layers. Baris 9 mencari layer bernama Petak_KPH. Bila Anda mengganti nama layer itu, pencarian menghasilkan daftar kosong dan layar menampilkan IndexError yang sama. Samakan nama di baris 9 dengan nama layer yang sekarang. Skrip 17.5 tidak mengubah nama layer, jadi skrip ini tetap bisa dijalankan sesudahnya.
  • Label G08 hilang setelah Skrip 17.2. Hasil render saya: bila skrip ini dijalankan setelah Skrip 17.1 saja, petak G08 berlabel belum diukur. Tetapi bila Skrip 17.2 sudah jalan, penyaji bertingkat tidak punya kelas untuk nilai kosong, sehingga G08 tidak digambar dan labelnya ikut hilang. Hanya 15 petak yang berlabel. Ekspresi coalesce tetap benar, seperti terlihat pada hasil uji skrip (baris 39). Yang berbeda adalah petak itu tidak dirender. Menurut saya, label hanya dibuat untuk fitur yang digambar. [kemungkinan]
  • FOLDER tidak dipakai. Jalur yang salah tidak berpengaruh di skrip ini.

Latihan variasi. Ubah label menjadi kode petak diikuti jenisnya, dalam satu baris, dipisah - . Contoh: A01 - Jati.

Contoh jawaban. Ganti baris 11-12 dengan satu baris:

PYTHON
ekspresi = '"ID_Petak" || \' - \' || "Jenis"'

Layar menampilkan A01 -> 'A01 - Jati' dan G08 -> 'G08 - Agroforestri'.

Skrip 17.5: Menyusun lembar peta A4 #

Berkas skrip_4_4_layout.py. Di dalam berkasnya tertulis "Skrip 4.4". Skrip ini yang terpanjang kedua di bab ini, tetapi isinya berulang. Ia memasang enam item satu per satu, dengan pola yang hampir sama.

Tujuan. Menyusun layout peta A4 lanskap yang lengkap (peta, judul, legenda, batang skala, panah utara, catatan), lalu mengekspornya ke PDF dan PNG.

Bahan. Layer hasil Skrip 17.1, 17.2, dan 17.4 sudah ada di proyek: Petak_KPH, Hotspot_Nasional, Sungai, Jalan, dan Belum diukur (dari Skrip 17.2). Skrip ini mengekspor dua berkas ke FOLDER: Peta_KPH.pdf dan Peta_KPH.png.

Lembar peta dibangun dari item: kotak-kotak yang ditaruh di halaman, masing-masing berisi satu hal. Letak dan ukuran item diukur dalam milimeter. Titik acuannya pojok kiri atas halaman. Bayangkan menempel stiker di kertas A4 dengan penggaris.

Ilustrasi 17.8: Letak item di halaman A4
Skema halaman A4 lanskap 297 kali 210 milimeter berisi judul di atas, peta di kiri, legenda dan panah utara di kanan, batang skala di bawah peta, dan catatan di dasar halaman

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

Ilustrasi 17.9: Alur skrip 17.5
Skema sembilan tahap skrip: buat layout, item peta, judul, legenda manual, batang skala, panah utara, catatan, ekspor PDF dan PNG, cetak hasil

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.4: menyusun layout peta A4 lanskap (peta, judul, legenda, skala, panah utara) dan mengekspornya
from qgis.core import (QgsProject, QgsPrintLayout, QgsLayoutItemMap, QgsLayoutItemLabel, QgsLayoutItemLegend,
                       QgsLayoutItemScaleBar, QgsLayoutItemPicture, QgsLayoutItemShape, QgsLayoutPoint, QgsLayoutSize,
                       QgsLayoutExporter, QgsRectangle, QgsApplication, QgsLayerTree, QgsLegendStyle,
                       QgsTextFormat, Qgis, QgsMapLayerLegendUtils)
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QColor, QFont
import os

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda
proyek = QgsProject.instance()
mm = Qgis.LayoutUnit.Millimeters

lama = proyek.layoutManager().layoutByName("Peta_KPH_A4")
if lama is not None:                             # dijalankan ulang: buang layout lama agar nama tidak bentrok
    proyek.layoutManager().removeLayout(lama)
tata = QgsPrintLayout(proyek)
tata.initializeDefaults()                       # halaman A4 lanskap (297 x 210 mm)
tata.setName("Peta_KPH_A4")
proyek.layoutManager().addLayout(tata)

# 1. Peta: ukuran 190 x 140 mm pada skala 1:3000
peta = QgsLayoutItemMap(tata)
peta.setCrs(proyek.crs())
peta.attemptMove(QgsLayoutPoint(10, 28, mm))
peta.attemptResize(QgsLayoutSize(190, 140, mm))   # ukuran dan letak diatur lebih dulu
peta.zoomToExtent(QgsRectangle(312000, 9996000, 312400, 9996400))   # pas ke area KPH
peta.setScale(3000)                               # skala diatur terakhir
peta.setFrameEnabled(True)
tata.addLayoutItem(peta)

# 2. Judul
judul = QgsLayoutItemLabel(tata)
judul.setText("PETA KPH CONTOH: VOLUME KAYU PER PETAK")
fmt = QgsTextFormat()
fmt.setFont(QFont("Arial", 18, QFont.Weight.Bold))
fmt.setSize(18)
judul.setTextFormat(fmt)
judul.attemptMove(QgsLayoutPoint(10, 8, mm))
judul.attemptResize(QgsLayoutSize(277, 14, mm))
tata.addLayoutItem(judul)

# 3. Legenda: dibuat manual agar nama entrinya ramah dibaca
legenda = QgsLayoutItemLegend(tata)
legenda.setLinkedMap(peta)
legenda.setSyncMode(Qgis.LegendSyncMode.Manual)   # isi legenda tidak ikut berubah otomatis
akar = legenda.model().rootGroup()
nama_baru = {"Petak_KPH": "Volume kayu (m3/ha)", "Hotspot_Nasional": "Titik panas",
             "Sungai": "Sungai", "Jalan": "Jalan", "Belum diukur": "Petak belum diukur"}
for simpul in list(akar.children()):
    if simpul.name() in nama_baru:
        baru = nama_baru[simpul.name()]
        if len(legenda.model().layerLegendNodes(simpul)) == 1:   # entri tunggal: ganti label entri (nama layer di proyek tetap)
            QgsMapLayerLegendUtils.setLegendNodeUserLabel(simpul, 0, baru)
            legenda.model().refreshLayerLegend(simpul)
        else:                                                    # beberapa entri: ganti judul kelompoknya
            simpul.setCustomProperty("legend/title-label", baru)
legenda.setTitle("Legenda")
legenda.attemptMove(QgsLayoutPoint(205, 28, mm))
tata.addLayoutItem(legenda)

# 4. Batang skala: 2 segmen, tiap segmen 100 m
skala = QgsLayoutItemScaleBar(tata)
skala.setLinkedMap(peta)
skala.setStyle("Single Box")
skala.setUnits(Qgis.DistanceUnit.Meters)
skala.setUnitLabel("m")
skala.setNumberOfSegments(2)
skala.setNumberOfSegmentsLeft(0)
skala.setUnitsPerSegment(100)
skala.attemptMove(QgsLayoutPoint(12, 174, mm))
tata.addLayoutItem(skala)

# 5. Panah utara (gambar SVG bawaan QGIS)
panah = QgsLayoutItemPicture(tata)
for folder in QgsApplication.svgPaths():
    berkas = os.path.join(folder, "arrows", "NorthArrow_02.svg")
    if os.path.exists(berkas):
        panah.setPicturePath(berkas)
        break
panah.attemptMove(QgsLayoutPoint(270, 28, mm))
panah.attemptResize(QgsLayoutSize(15, 15, mm))
panah.setLinkedMap(peta)                          # panah ikut arah utara peta
tata.addLayoutItem(panah)

# 6. Teks skala angka, CRS, sumber, dan penulis
catatan = QgsLayoutItemLabel(tata)
catatan.setText("Skala 1:3.000 (A4). Sistem koordinat: WGS 84 / UTM zona 49S (EPSG:32749).\n"
                "Sumber: data sintetis latihan, bukan kondisi lapangan. Penyusun: Analis Muda, KPH Contoh.")
fmt2 = QgsTextFormat()
fmt2.setFont(QFont("Arial"))
fmt2.setSize(8)
catatan.setTextFormat(fmt2)
catatan.attemptMove(QgsLayoutPoint(10, 192, mm))
catatan.attemptResize(QgsLayoutSize(277, 12, mm))
tata.addLayoutItem(catatan)

# 7. Ekspor
pengekspor = QgsLayoutExporter(tata)
hasil_pdf = pengekspor.exportToPdf(os.path.join(FOLDER, "Peta_KPH.pdf"), QgsLayoutExporter.PdfExportSettings())
pengaturan = QgsLayoutExporter.ImageExportSettings()
pengaturan.dpi = 150
hasil_png = pengekspor.exportToImage(os.path.join(FOLDER, "Peta_KPH.png"), pengaturan)
print("PDF:", hasil_pdf, " PNG:", hasil_png)
print("Skala peta: 1:%d" % round(peta.scale()), " Tinggi peta (mm):", round(peta.rect().height()))

Penjelasan baris demi baris. Enam item memakai pola yang sama: buat item, atur letak dan ukuran, isi, lalu addLayoutItem. Karena itu tabel ini dikelompokkan per item.

BarisTulisanArtinya
2-8from ... import (...), import osPanggil banyak perkakas layout. Lima di antaranya tidak dipakai di skrip ini: QgsLayoutItemShape, QgsLayerTree, QgsLegendStyle, Qt, dan QColor. Impor yang tersisa tidak berpengaruh. Perkakas QgsMapLayerLegendUtils dipakai untuk mengganti tulisan entri legenda.
10-12FOLDER, proyek, mmFolder kerja, proyek yang terbuka, dan satuan milimeter yang disimpan di kotak mm agar tidak ditulis panjang.
14-16lama = ...layoutByName("Peta_KPH_A4"), if lama is not None:, removeLayout(lama)Cari layout bernama Peta_KPH_A4 di proyek. Bila ada (is not None), buang dulu. Dengan begitu skrip aman dijalankan ulang, karena dua layout tidak boleh sama namanya.
17tata = QgsPrintLayout(proyek)Buat layout baru di proyek ini.
18tata.initializeDefaults()Isi pengaturan awal: satu halaman A4 lanskap, 297 kali 210 mm.
19-20tata.setName("Peta_KPH_A4"), ...addLayout(tata)Beri nama layout, lalu daftarkan ke manajer layout proyek. Nama ini dipakai Skrip 17.6.
23-24peta = QgsLayoutItemMap(tata), peta.setCrs(proyek.crs())Buat item peta. Pakai sistem koordinat yang sama dengan proyek.
25-26attemptMove(QgsLayoutPoint(10, 28, mm)), attemptResize(QgsLayoutSize(190, 140, mm))Letakkan pojok kiri atas peta pada 10 mm dari kiri dan 28 mm dari atas. Ukurannya 190 kali 140 mm. Kata attempt berarti "coba": QGIS bisa menolak bila ada penyebab lain.
27peta.zoomToExtent(QgsRectangle(...))Arahkan isi peta ke satu persegi panjang: kiri, bawah, kanan, atas, dalam koordinat UTM.
28peta.setScale(3000)Atur skala menjadi 1:3000. Komentar mengingatkan: skala diatur terakhir.
29-30setFrameEnabled(True), tata.addLayoutItem(peta)Beri bingkai di sekeliling peta, lalu tambahkan item ke halaman.
33-38judul = QgsLayoutItemLabel(tata) ... setTextFormat(fmt)Buat item label. Isi tulisannya, atur huruf Arial 18 tebal, lalu pasang format. QFont.Weight.Bold berarti tebal.
39-41attemptMove, attemptResize, addLayoutItemLetakkan judul di pojok 10 mm dari kiri dan 8 mm dari atas, selebar 277 mm. Lalu tambahkan.
44-46legenda = QgsLayoutItemLegend(tata), setLinkedMap, setSyncMode(...Manual)Buat legenda, hubungkan ke item peta, lalu pilih mode manual. Mode manual berarti isi legenda tidak ikut berubah otomatis, jadi nama entrinya boleh diganti.
47akar = legenda.model().rootGroup()Ambil isi legenda, yaitu daftar entri layer.
48-49nama_baru = {...}Kamus: nama layer lama dan nama ramah yang diinginkan, misalnya Petak_KPH menjadi Volume kayu (m3/ha).
50-57for simpul in list(akar.children()): ...Ulangi untuk tiap entri legenda. Bila nama layer ada di kamus, ambil nama barunya (baris 52). Layer yang hanya punya satu entri (baris 53) diganti tulisan entrinya dengan setLegendNodeUserLabel, dan nama layer di proyek tetap. Layer berentri banyak, yaitu Petak_KPH, diganti judul kelompoknya lewat properti legend/title-label (baris 56-57). Fungsi list(...) membuat salinan daftar agar aman dibaca sambil diubah.
58-60setTitle, attemptMove, addLayoutItemBeri judul legenda, letakkan pada 205 mm dari kiri dan 28 mm dari atas, lalu tambahkan.
63-70skala = QgsLayoutItemScaleBar(tata) ... setUnitsPerSegment(100)Buat batang skala: gaya Single Box, satuan meter, 2 segmen, tiap segmen 100 meter, dan tanpa segmen di kiri nol. Total panjang 200 meter.
71-72attemptMove, addLayoutItemLetakkan di bawah peta, lalu tambahkan.
75panah = QgsLayoutItemPicture(tata)Buat item gambar untuk panah utara.
76-80for folder in QgsApplication.svgPaths(): ... breakCari berkas NorthArrow_02.svg di semua folder gambar bawaan QGIS. Begitu ditemukan, pasang sebagai gambar, lalu break menghentikan pencarian.
81-84attemptMove, attemptResize, setLinkedMap(peta), addLayoutItemLetakkan panah di kanan atas, ukuran 15 kali 15 mm. Hubungkan ke peta supaya panah ikut arah utara peta.
87-93catatan = QgsLayoutItemLabel(tata) ... setTextFormat(fmt2)Buat label kedua berisi skala, sistem koordinat, sumber, dan penyusun. Dua potongan teks disambung otomatis oleh tanda kurung, dan \n memecah baris. Huruf Arial ukuran 8.
94-96attemptMove, attemptResize, addLayoutItemLetakkan di dasar halaman, selebar 277 mm.
99pengekspor = QgsLayoutExporter(tata)Buat pengekspor untuk layout ini.
100hasil_pdf = pengekspor.exportToPdf(...)Ekspor ke PDF. Hasilnya angka: 0 berarti berhasil.
101-103pengaturan = ...ImageExportSettings(), pengaturan.dpi = 150, exportToImage(...)Ekspor ke PNG pada 150 dpi. Dibahas lebih lanjut di Skrip 17.6.
104-105print(...)Cetak hasil ekspor, lalu skala dan tinggi peta. round(peta.scale()) membulatkan skala; %d mencetaknya sebagai bilangan bulat.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2, urutan 17.1, 17.2, 17.4, 17.5):

HASIL
ERROR 6: The PNG driver does not support update access to existing datasets.
PDF: 0  PNG: 0
Skala peta: 1:3000  Tinggi peta (mm): 140

Baris ERROR 6 berasal dari pustaka gambar GDAL dan tidak berarti ekspor gagal. Buktinya ada di baris di bawahnya: PDF: 0 PNG: 0 berarti kedua ekspor berhasil, dan kedua berkas ada di folder kerja. Pesan itu muncul pada setiap ekspor PNG di uji ini.

Lembar yang dihasilkan memuat judul "PETA KPH CONTOH: VOLUME KAYU PER PETAK", peta petak berwarna dengan label, legenda, batang skala 0 sampai 200 meter, panah utara, dan catatan sumber. Entri legenda tertulis Volume kayu (m3/ha) dan Petak belum diukur. Nama layer di proyek tidak berubah: tetap Belum diukur, Hotspot_Nasional, Jalan, Petak_KPH, dan Sungai.

Bagian yang boleh diubah.

  • Baris 25-26 dan seterusnya, angka letak. Semua angka di QgsLayoutPoint(x, y, mm) dan QgsLayoutSize(lebar, tinggi, mm) dalam milimeter. Ubah untuk menggeser atau memperbesar item.
  • Baris 27, ekstensi. Ganti keempat koordinat dengan area yang ingin ditampilkan, dalam satuan peta.
  • Baris 28, skala. Ganti 3000, misalnya 4000 untuk area lebih luas.
  • Baris 34, judul. Ganti teks judul.
  • Baris 48-49, nama entri legenda. Ganti isi kamus. Kunci kiri harus persis sama dengan nama layer.
  • Baris 67-70, batang skala. Ganti jumlah segmen dan panjang tiap segmen.
  • Baris 88-89, catatan. Ganti teks catatan.
  • Baris 102, dpi. Ganti 150 untuk resolusi PNG lain.

Jebakan.

  • Legenda manual hanya mengganti tulisan, bukan nama layer. Baris 50-57 mengubah tulisan di legenda, sedangkan nama layer di panel Layers tetap Petak_KPH, Hotspot_Nasional, dan seterusnya. Karena itu skrip lain yang mencari layer menurut nama tetap bisa jalan. Bila Anda mengganti baris 54 atau 57 dengan simpul.setName(...), nama layer di proyek ikut berubah. Pada uji, menjalankan Skrip 17.2 sesudah itu menghasilkan IndexError: list index out of range. Kunci kiri di kamus baris 48-49 harus memakai nama layer yang sebenarnya.
  • Dijalankan ulang. Layout bernama sama tidak boleh ada dua. Baris 14-16 mencari layout Peta_KPH_A4 yang lama dan membuangnya sebelum layout baru dibuat, jadi skrip aman diulang. Tanpa tiga baris itu, QGIS menolak layout kedua dan skrip gagal dengan RuntimeError: wrapped C/C++ object of type QgsPrintLayout has been deleted. Ingat bahwa layout lama ikut terbuang. Bila Anda sudah mengubah layout itu dengan tangan, ubahan itu hilang. Untuk menyimpan keduanya, ganti nama di baris 19.
  • Urutan mengatur peta. Bila Anda menukar baris 27 dan 28 (skala diatur dulu, lalu zoomToExtent), skala berubah mengikuti area. Uji saya: skala menjadi 1:2857, bukan 1:3000. Atur ukuran dulu, lalu zoomToExtent, lalu setScale.
  • Angka tetap. Ekstensi di baris 27 hanya cocok dengan data latihan. Tulisan Skala 1:3.000 di baris 88 diketik manual. Bila Anda mengganti skala di baris 28, teks catatan tidak ikut berubah. Samakan dua-duanya.
  • Panah utara tidak ketemu. Bila nama berkas SVG di baris 77 salah, skrip tidak menimbulkan galat. Uji: panah.picturePath() kosong (''), dan panah tidak tampil. Cek bahwa panah muncul di hasil.
  • Layer raster dari Skrip 17.3 ikut. Bila Skrip 17.3 sudah dijalankan di proyek yang sama, layer bayangan bukit menutup petak. Hasil uji: peta abu-abu. Legenda juga memuat entri DTM_Bayangan dan DTM_Drone dengan nama aslinya.
  • Pesan ERROR 6 tidak berbahaya. Lihat bagian Hasil.

Latihan variasi. Ubah peta menjadi skala 1:4000, dan perbarui tulisan di catatan agar tetap benar.

Contoh jawaban. Ganti dua baris:

PYTHON
peta.setScale(4000)                               # skala diatur terakhir
PYTHON
catatan.setText("Skala 1:4.000 (A4). Sistem koordinat: WGS 84 / UTM zona 49S (EPSG:32749).\n"

Layar menampilkan Skala peta: 1:4000 Tinggi peta (mm): 140.

Skrip 17.6: Ekspor ke PDF dan PNG tiga resolusi #

Berkas skrip_4_5_ekspor.py. Di dalam berkasnya tertulis "Skrip 4.5". Skrip ini pendek, dan sebagian besar logikanya sudah Anda kenal dari baris 99-105 Skrip 17.5.

Tujuan. Mengekspor layout Peta_KPH_A4 ke satu PDF dan tiga PNG dengan resolusi berbeda, lalu mencatat ukuran tiap berkas.

Bahan. Layout Peta_KPH_A4 dari Skrip 17.5 sudah ada di proyek. Hasilnya empat berkas di FOLDER: Peta_KPH_A4.pdf, Peta_KPH_72.png, Peta_KPH_150.png, dan Peta_KPH_300.png.

dpi (dots per inch) adalah banyaknya titik gambar tiap inci kertas. Bayangkan sulaman: makin rapat jahitannya, makin halus gambarnya, tetapi makin besar bahan yang dipakai. PDF menyimpan huruf dan garis sebagai rumus (vektor), jadi tetap tajam di ukuran berapa pun. PNG menyimpan kotak-kotak piksel, jadi ketajamannya ditentukan dpi.

Ilustrasi 17.10: Ukuran PNG pada tiga resolusi
Skema tiga persegi panjang sebanding yang menunjukkan ukuran PNG: 841 kali 595 piksel pada 72 dpi, 1753 kali 1240 pada 150 dpi, dan 3507 kali 2480 pada 300 dpi
Ilustrasi 17.11: Alur skrip 17.6
Skema empat tahap skrip: ambil layout, buat pengekspor, ekspor PDF dan cetak ukuran, ulangi untuk tiga resolusi PNG

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.5: mengekspor layout Peta_KPH_A4 ke PDF dan PNG pada beberapa resolusi, lalu mencatat ukuran berkas
import os
from qgis.core import QgsProject, QgsLayoutExporter

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda

tata = QgsProject.instance().layoutManager().layoutByName("Peta_KPH_A4")
assert tata is not None, "Layout Peta_KPH_A4 belum ada; jalankan skrip 4.4 lebih dulu"
pengekspor = QgsLayoutExporter(tata)

# PDF: huruf dan garis tetap berupa vektor
pdf = os.path.join(FOLDER, "Peta_KPH_A4.pdf")
hasil = pengekspor.exportToPdf(pdf, QgsLayoutExporter.PdfExportSettings())
print("PDF  hasil=%s  ukuran=%d byte" % (hasil, os.path.getsize(pdf)))

# PNG: tiga resolusi
for dpi in (72, 150, 300):
    pengaturan = QgsLayoutExporter.ImageExportSettings()
    pengaturan.dpi = dpi
    png = os.path.join(FOLDER, "Peta_KPH_%d.png" % dpi)
    hasil = pengekspor.exportToImage(png, pengaturan)
    from qgis.PyQt.QtGui import QImage
    gambar = QImage(png)
    print("PNG %3d dpi  hasil=%s  %d x %d piksel  %d byte" % (dpi, hasil, gambar.width(), gambar.height(), os.path.getsize(png)))

Penjelasan baris demi baris. Hanya hal yang baru dijelaskan.

BarisTulisanArtinya
2-3import os, from qgis.core import ...Panggil os untuk urusan jalur dan ukuran berkas, serta perkakas ekspor QGIS.
7tata = QgsProject.instance().layoutManager().layoutByName("Peta_KPH_A4")Ambil layout menurut namanya dari manajer layout. Bila nama tidak ada, hasilnya None.
8assert tata is not None, "Layout ... belum ada; ..."Pemeriksa (Bab 10). Bila layout tidak ditemukan, skrip berhenti dengan AssertionError dan pesan yang menyuruh Anda menjalankan Skrip 17.5 lebih dulu. Pesan itu menyebut "skrip 4.4", yaitu nomor di berkasnya.
9pengekspor = QgsLayoutExporter(tata)Buat pengekspor.
12pdf = os.path.join(FOLDER, "Peta_KPH_A4.pdf")Gabungkan folder dan nama berkas menjadi satu jalur, dengan pemisah yang benar untuk komputer Anda.
13hasil = pengekspor.exportToPdf(pdf, ...PdfExportSettings())Ekspor ke PDF dengan pengaturan bawaan. Angka hasil disimpan di hasil.
14print("PDF hasil=%s ukuran=%d byte" % (hasil, os.path.getsize(pdf)))Cetak angka hasil dan ukuran berkas dalam byte. %s untuk teks atau angka apa pun, %d untuk bilangan bulat.
17for dpi in (72, 150, 300):Ulangi tiga kali. Di tiap putaran, dpi berisi satu nilai dari daftar.
18-19pengaturan = ...ImageExportSettings(), pengaturan.dpi = dpiBuat pengaturan gambar baru, lalu isi resolusinya.
20png = os.path.join(FOLDER, "Peta_KPH_%d.png" % dpi)Susun nama berkas yang memuat angka dpi, misalnya Peta_KPH_150.png.
21hasil = pengekspor.exportToImage(png, pengaturan)Ekspor ke PNG.
22from qgis.PyQt.QtGui import QImagePanggil perkakas pembaca gambar. Menaruhnya di dalam perulangan kurang lazim. Hasilnya tetap benar, karena Python hanya mengimpor sekali. Anda boleh memindahkannya ke atas.
23gambar = QImage(png)Buka PNG yang baru jadi, untuk membaca ukurannya.
24print("PNG %3d dpi hasil=%s %d x %d piksel %d byte" % (...))Cetak dpi, angka hasil, lebar dan tinggi piksel, dan ukuran berkas. %3d memberi lebar tiga karakter agar sejajar.

Hasil. Layar menampilkan (berdasarkan uji pada QGIS 4.0.2, urutan 17.1, 17.2, 17.4, 17.5, 17.6):

HASIL
PDF  hasil=0  ukuran=207837 byte
ERROR 6: The PNG driver does not support update access to existing datasets.
PNG  72 dpi  hasil=0  841 x 595 piksel  68981 byte
ERROR 6: The PNG driver does not support update access to existing datasets.
PNG 150 dpi  hasil=0  1753 x 1240 piksel  181906 byte
ERROR 6: The PNG driver does not support update access to existing datasets.
PNG 300 dpi  hasil=0  3507 x 2480 piksel  404416 byte

Ukuran piksel selalu sama untuk dpi yang sama: 841 kali 595, 1753 kali 1240, dan 3507 kali 2480. Ukuran byte bergantung isi peta dan bisa sedikit berbeda di komputer Anda. Pesan ERROR 6 sama dengan di Skrip 17.5 dan tidak berbahaya.

Bagian yang boleh diubah.

  • Baris 7, nama layout. Harus persis sama dengan nama di Skrip 17.5. Ubah juga nama di pesan baris 8.
  • Baris 17, daftar dpi. Ganti isi tanda kurung, misalnya (96, 200).
  • Baris 12 dan 20, nama berkas. Ganti nama sesuka Anda. Nama di baris 12 beda dengan Peta_KPH.pdf dari Skrip 17.5, jadi hasilnya dua PDF.

Jebakan.

  • Layout belum dibuat. Bila Skrip 17.5 belum jalan, layoutByName mengembalikan None, dan baris 8 menghentikan skrip dengan AssertionError: Layout Peta_KPH_A4 belum ada; .... Tanpa baris itu, skrip terus berjalan dan gagal belakangan dengan FileNotFoundError pada nama berkas PDF. Pesan itu menyesatkan, karena masalah sebenarnya ada di layout, bukan di berkas.
  • Berkas lama tidak membuktikan ekspor berhasil. Folder bisa masih berisi PDF dan PNG dari proses sebelumnya, sehingga ukuran berkas yang tercetak bisa berasal dari berkas lama. Yang menentukan adalah angka hasil. Nilai 0 berarti berhasil, nilai selain 0 berarti gagal. Selalu baca angka itu.
  • Berkas lama ditimpa. Berkas bernama sama diganti tanpa tanya. Pindahkan hasil penting sebelum menjalankan ulang.
  • Resolusi sangat tinggi. Ukuran PNG naik cepat bersama dpi. Pada 300 dpi A4 sudah 3507 kali 2480 piksel. Tambah dpi hanya bila perlu.

Latihan variasi. Ekspor PNG hanya pada 96 dan 200 dpi, tanpa PDF.

Contoh jawaban. Hapus baris 11-14 dan ganti baris 17:

PYTHON
for dpi in (96, 200):

Layar menampilkan PNG 96 dpi hasil=0 1122 x 793 piksel dan PNG 200 dpi hasil=0 2338 x 1653 piksel, masing-masing diikuti ukuran byte.

Skrip 17.7: Proyek mini dari data mentah sampai PDF #

Berkas skrip_4_6_jembatan.py. Di dalam berkasnya tertulis "Skrip 4.6". Skrip ini disebut "jembatan", karena menyambungkan kelas dasar dengan kelas menengah. Ia memadukan seleksi, alat Processing, simbologi, layout, dan ekspor dalam satu berkas.

Tujuan. Memeriksa sistem koordinat titik panas, lalu mengubahnya ke UTM. Setelah itu, menyeleksi petak jati bervolume besar dan membuat sempadan sungai 25 meter. Petak dan titik panas dipotong dengan sempadan itu. Terakhir, skrip membuat peta dan menyimpan proyek.

Bahan. Folder kerja berisi Hotspot_derajat.gpkg, Petak_KPH.gpkg, dan Sungai_Jalan.gpkg. Skrip berdiri sendiri, tetapi mengosongkan proyek. Hasilnya Peta_Sempadan.pdf dan KPH_Contoh_B4.qgz di FOLDER.

Lima istilah baru, yang dijelaskan lebih lengkap di Bab 18:

  • Reproject: mengubah sistem koordinat sebuah layer.
  • Buffer: zona selebar jarak tertentu di sekeliling objek.
  • Clip: memotong sebuah layer dengan bentuk layer lain.
  • processing.run: perintah memanggil alat Processing dari skrip.
  • assert: pemeriksa yang menghentikan skrip bila syaratnya tidak terpenuhi.

Sempadan sungai di skrip ini hanya angka latihan, 25 meter. Itu bukan ketentuan resmi.

Ilustrasi 17.12: Alur skrip 17.7
Skema dua belas tahap skrip: kosongkan proyek, fungsi muat, cek CRS titik panas, reproject, muat petak dan sungai, seleksi jati, buffer sungai, clip petak, clip titik panas, simbologi, layout, ekspor dan simpan

Skrip utuh:

PYTHONUnduh berkas
# Skrip 4.6 (Bab Jembatan): proyek mini terpadu B1-B4 dan pembuka geoprocessing.
# Alur: periksa CRS, reproject titik panas, seleksi atribut, buffer sungai, clip, simbologi, layout, ekspor.
import os
import processing
from qgis.core import (QgsProject, QgsVectorLayer, QgsCoordinateReferenceSystem, QgsFillSymbol,
                       QgsSingleSymbolRenderer, QgsMarkerSymbol, QgsLineSymbol, QgsPrintLayout,
                       QgsLayoutItemMap, QgsLayoutItemLabel, QgsLayoutItemLegend, QgsLayoutItemScaleBar,
                       QgsLayoutPoint, QgsLayoutSize, QgsLayoutExporter, QgsRectangle, QgsTextFormat, Qgis)
from qgis.PyQt.QtGui import QFont

FOLDER = r"D:/KPH_Contoh"   # ganti dengan folder kerja Anda
JARAK_SEMPADAN = 25         # meter; angka latihan, bukan ketentuan resmi

proyek = QgsProject.instance()
proyek.clear()
proyek.setCrs(QgsCoordinateReferenceSystem("EPSG:32749"))

def muat(berkas, lapisan, nama):
    layer = QgsVectorLayer("%s/%s|layername=%s" % (FOLDER, berkas, lapisan), nama, "ogr")
    assert layer.isValid(), "Layer tidak terbaca: " + nama
    return layer

# 1. Periksa CRS: titik panas kiriman kantor lain masih berderajat (B1)
mentah = muat("Hotspot_derajat.gpkg", "Hotspot_derajat", "Hotspot_mentah")
print("CRS titik panas mentah:", mentah.crs().authid(), "| satuan:", mentah.crs().mapUnits())

# 2. Reproject ke UTM 49S (alat Processing pertama)
hasil = processing.run("native:reprojectlayer", {
    "INPUT": mentah, "TARGET_CRS": QgsCoordinateReferenceSystem("EPSG:32749"), "OUTPUT": "memory:Titik_Panas"})
titik = hasil["OUTPUT"]
print("Titik panas setelah reproject:", titik.featureCount(), "titik,", titik.crs().authid())

# 3. Data utama
petak = muat("Petak_KPH.gpkg", "Petak_KPH", "Petak KPH")
sungai = muat("Sungai_Jalan.gpkg", "Sungai", "Sungai")

# 4. Seleksi atribut (B2): petak jati bervolume tinggi
petak.selectByExpression('"Jenis" = \'Jati\' AND "Volume_m3ha" > 150')
print("Jati dengan volume > 150 m3/ha:", sorted(f["ID_Petak"] for f in petak.selectedFeatures()))
petak.removeSelection()

# 5. Buffer sungai (alat Processing kedua): sempadan contoh 25 m, digabung jadi satu bidang
sempadan = processing.run("native:buffer", {
    "INPUT": sungai, "DISTANCE": JARAK_SEMPADAN, "SEGMENTS": 16, "DISSOLVE": True,
    "OUTPUT": "memory:Sempadan"})["OUTPUT"]
luas_sempadan = sum(f.geometry().area() for f in sempadan.getFeatures())
print("Luas buffer sungai: %.0f m2 (%.2f ha)" % (luas_sempadan, luas_sempadan / 10000))

# 6. Clip petak dengan buffer: bagian petak yang masuk sempadan
petak_sempadan = processing.run("native:clip", {
    "INPUT": petak, "OVERLAY": sempadan, "OUTPUT": "memory:Petak_di_sempadan"})["OUTPUT"]
total = 0
for f in sorted(petak_sempadan.getFeatures(), key=lambda x: x["ID_Petak"]):
    luas = f.geometry().area()
    total += luas
    print("   %s  %-6s %7.0f m2" % (f["ID_Petak"], f["Jenis"], luas))
print("Total petak dalam sempadan: %.0f m2 (%.2f ha), %d petak" % (total, total / 10000, petak_sempadan.featureCount()))

# 7. Clip titik panas dengan buffer: titik panas yang berada di dalam sempadan
titik_dalam = processing.run("native:clip", {
    "INPUT": titik, "OVERLAY": sempadan, "OUTPUT": "memory:Titik_di_sempadan"})["OUTPUT"]
print("Titik panas di dalam sempadan:", titik_dalam.featureCount(), "dari", titik.featureCount())

# 8. Simbologi (B4) dan penyusunan layer; urutan: yang ditambah belakangan berada di atas
petak.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
    {"color": "#E8F0D8", "outline_color": "#404040", "outline_width": "0.3"})))
sempadan.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
    {"color": "#8FB8E0", "outline_color": "#2B6CB0", "outline_width": "0.2", "style": "solid"})))
petak_sempadan.setRenderer(QgsSingleSymbolRenderer(QgsFillSymbol.createSimple(
    {"color": "#F2A65A", "outline_color": "#8A4B00", "outline_width": "0.3"})))
sungai.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol.createSimple({"line_color": "#2B6CB0", "line_width": "0.8"})))
titik.setRenderer(QgsSingleSymbolRenderer(QgsMarkerSymbol.createSimple(
    {"name": "circle", "color": "#E03C31", "outline_color": "#FFFFFF", "size": "2.6"})))
for layer in (petak, sempadan, petak_sempadan, sungai, titik):
    proyek.addMapLayer(layer)
petak_sempadan.setName("Petak di dalam sempadan")
sempadan.setName("Sempadan sungai %g m" % JARAK_SEMPADAN)
titik.setName("Titik panas")

# 9. Layout ringkas dan ekspor
mm = Qgis.LayoutUnit.Millimeters
tata = QgsPrintLayout(proyek)
tata.initializeDefaults()
tata.setName("Peta_Sempadan")
proyek.layoutManager().addLayout(tata)
peta = QgsLayoutItemMap(tata)
peta.setCrs(proyek.crs())
peta.attemptMove(QgsLayoutPoint(10, 28, mm))
peta.attemptResize(QgsLayoutSize(190, 140, mm))
peta.zoomToExtent(QgsRectangle(312000, 9996000, 312400, 9996400))
peta.setScale(3000)
peta.setFrameEnabled(True)
tata.addLayoutItem(peta)
judul = QgsLayoutItemLabel(tata)
judul.setText("SEMPADAN SUNGAI DI KPH CONTOH (CONTOH %g M)" % JARAK_SEMPADAN)
fmt = QgsTextFormat()
fmt.setFont(QFont("Arial", 16, QFont.Weight.Bold))
fmt.setSize(16)
judul.setTextFormat(fmt)
judul.attemptMove(QgsLayoutPoint(10, 10, mm))
judul.attemptResize(QgsLayoutSize(277, 12, mm))
tata.addLayoutItem(judul)
legenda = QgsLayoutItemLegend(tata)
legenda.setLinkedMap(peta)
legenda.setTitle("Legenda")
legenda.attemptMove(QgsLayoutPoint(205, 28, mm))
tata.addLayoutItem(legenda)
skala = QgsLayoutItemScaleBar(tata)
skala.setLinkedMap(peta)
skala.setStyle("Single Box")
skala.setUnits(Qgis.DistanceUnit.Meters)
skala.setUnitLabel("m")
skala.setNumberOfSegments(2)
skala.setNumberOfSegmentsLeft(0)
skala.setUnitsPerSegment(100)
skala.attemptMove(QgsLayoutPoint(12, 174, mm))
tata.addLayoutItem(skala)
hasil_pdf = QgsLayoutExporter(tata).exportToPdf(os.path.join(FOLDER, "Peta_Sempadan.pdf"), QgsLayoutExporter.PdfExportSettings())
print("Ekspor PDF:", hasil_pdf, "(0 = berhasil)")
proyek.write(os.path.join(FOLDER, "KPH_Contoh_B4.qgz"))
print("Proyek disimpan:", os.path.exists(os.path.join(FOLDER, "KPH_Contoh_B4.qgz")))

Penjelasan baris demi baris. Bagian layout (baris 80-117) mengulang pola Skrip 17.5 dan dirangkum saja.

BarisTulisanArtinya
1-9komentar dan importDua komentar pembuka, lalu perkakas: os, processing (pemanggil alat Processing), dan banyak perkakas QGIS.
11-12FOLDER, JARAK_SEMPADAN = 25Folder kerja dan jarak sempadan dalam meter. Nama kotak dengan huruf besar menandakan nilai tetap, yang diatur di awal.
14-16proyek.clear() dan setCrsPeringatan: clear() mengosongkan seluruh proyek. Lalu atur CRS proyek ke EPSG:32749.
18-21def muat(...) dengan assertFungsi pemuat. Beda dengan Skrip 17.1, ia hanya membuat layer dan tidak menambahkannya ke proyek. Baris 20: bila layer.isValid() salah, skrip berhenti dengan pesan Layer tidak terbaca: diikuti nama layer.
24-25mentah = muat(...), print(... mapUnits())Muat titik panas berderajat, lalu cetak kode CRS dan satuan petanya.
28-31processing.run("native:reprojectlayer", {...})Panggil alat reproject. Isian pertama nama alat, isian kedua kamus pengaturan: layer masukan, CRS tujuan, dan OUTPUT berupa "memory:Titik_Panas", yaitu layer sementara di memori. Hasilnya kamus, dan layer baru diambil dengan ["OUTPUT"].
34-35petak = muat(...), sungai = muat(...)Muat petak dan sungai.
38petak.selectByExpression('"Jenis" = \'Jati\' AND "Volume_m3ha" > 150')Pilih petak yang jenisnya Jati dan volumenya lebih dari 150. Seleksi ini hanya menandai petak, tanpa mengubah data.
39-40print(... sorted(... for f in petak.selectedFeatures())), removeSelection()Cetak kode petak terpilih, urut abjad. Lalu hapus seleksi.
43-45sempadan = processing.run("native:buffer", {...})["OUTPUT"]Buat buffer sungai sejauh JARAK_SEMPADAN. SEGMENTS: 16 mengatur kehalusan sudut, dan DISSOLVE: True menggabungkan semua buffer menjadi satu bidang.
46-47luas_sempadan = sum(... for f in ...), print(...)Jumlahkan luas bidang buffer, lalu cetak dalam meter persegi dan hektare. Satu hektare adalah 10.000 meter persegi.
50-51petak_sempadan = processing.run("native:clip", {...})["OUTPUT"]Potong petak dengan bidang buffer. Yang tersisa hanya bagian petak di dalam sempadan.
52-57total = 0 dan perulangan forSiapkan penjumlah total bernilai 0. Ulangi untuk tiap potongan, urut kode petak. Tambahkan luasnya ke total (total += luas), lalu cetak satu baris. Terakhir cetak jumlahnya.
53key=lambda x: x["ID_Petak"]Aturan pengurutan: urutkan menurut kolom ID_Petak. lambda adalah fungsi mini satu baris (Bab 7).
60-62titik_dalam = processing.run("native:clip", ...)Potong titik panas dengan buffer yang sama, lalu cetak jumlahnya.
65-73petak.setRenderer(...) dan empat baris serupaBeri simbol tunggal pada lima layer. Pola sama dengan Skrip 17.1.
74-75for layer in (...): proyek.addMapLayer(layer)Tambahkan lima layer ke proyek. Urutan penambahan menentukan tumpukan: yang terakhir berada di atas.
76-78petak_sempadan.setName(...), sempadan.setName(...), titik.setName(...)Beri nama ramah pada tiga layer sementara. Nama sempadan memakai %g, yaitu format angka yang mencetak 25 sebagai 25. Nilainya diambil dari JARAK_SEMPADAN, jadi nama ikut berubah bila jarak diganti.
80-117mm = ... sampai tata.addLayoutItem(skala)Layout ringkas: peta, judul, legenda, dan batang skala. Pola sama dengan Skrip 17.5. Perbedaannya, legenda tidak dibuat manual (baris 103-107), dan tidak ada panah utara atau catatan. Judul peta (baris 95) juga memakai %g dan JARAK_SEMPADAN.
118-119hasil_pdf = QgsLayoutExporter(tata).exportToPdf(...)Ekspor PDF, lalu cetak angka hasil.
120proyek.write(os.path.join(FOLDER, "KPH_Contoh_B4.qgz"))Simpan proyek ke berkas .qgz.
121print("Proyek disimpan:", os.path.exists(...))Periksa bahwa berkas ada.

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

HASIL
CRS titik panas mentah: EPSG:4326 | satuan: 6
Titik panas setelah reproject: 12 titik, EPSG:32749
Jati dengan volume > 150 m3/ha: ['G05', 'G06', 'G07']
Luas buffer sungai: 29435 m2 (2.94 ha)
   A04  Jati       739 m2
   B01  Mahoni    1300 m2
   B02  Agroforestri    5736 m2
   B03  Akasia    5663 m2
   B04  Sengon     203 m2
   G01  Mahoni     134 m2
   G02  Sengon    5690 m2
   G03  Jati       558 m2
   G05  Jati      5612 m2
   G06  Jati      1438 m2
Total petak dalam sempadan: 27072 m2 (2.71 ha), 10 petak
Titik panas di dalam sempadan: 6 dari 12
Ekspor PDF: 0 (0 = berhasil)
Proyek disimpan: True

Cara membaca hasil. Angka 4326 adalah kode lintang-bujur (derajat).

Angka satuan: 6 adalah kode satuan peta untuk derajat. Uji saya: int(Qgis.DistanceUnit.Degrees) bernilai 6. Setelah reproject, 12 titik panas tetap 12, tetapi sistem koordinatnya UTM. Sepuluh dari 16 petak tersentuh sempadan, dengan luas total 27.072 meter persegi.

Bagian yang boleh diubah.

  • Baris 11, folder. Ganti jalur komputer Anda.
  • Baris 12, jarak sempadan. Ganti 25 menjadi angka lain. Nama layer sempadan dan judul peta ikut berubah. Komentar dan teks cetak lain tidak ikut (lihat Jebakan).
  • Baris 38, ambang volume. Ganti 150, atau ganti 'Jati' dengan jenis lain.
  • Baris 44, jumlah segmen. Ganti 16. Angka kecil membuat ujung buffer bersudut kasar.
  • Baris 65-73, warna simbol. Ganti kode warna.
  • Baris 90-91, ekstensi dan skala peta. Sama seperti Skrip 17.5.

Jebakan.

  • proyek.clear() mengosongkan proyek. Lihat kotak peringatan di awal skrip ini. Hasil uji: tujuh layer dan layout Peta_KPH_A4 hilang, diganti lima layer baru dan layout Peta_Sempadan.
  • Layer sementara tidak ikut tersimpan. Baris 120 menyimpan proyek, tetapi tiga layer hasil Processing hanya ada di memori. Saya menyimpan proyek lalu membukanya lagi di sesi baru. Hasilnya: Petak di dalam sempadan, Sempadan sungai 25 m, dan Titik panas ikut muncul, tetapi ketiganya kosong (0 fitur). Hanya Petak KPH dan Sungai yang berisi. Untuk menyimpan hasil, tulis keluaran ke berkas, misalnya "OUTPUT": FOLDER + "/Sempadan.gpkg" pada baris 45.
  • Jalur salah menghentikan skrip dengan pesan jelas. Baris 20 menghasilkan AssertionError: Layer tidak terbaca: Hotspot_mentah. Pesan di Skrip 17.1 menyebut nama berkas, sedangkan di sini nama layer. Periksa FOLDER dan nama berkas.
  • Nama layer dan judul mengikuti angka, teks lain tidak. Ganti JARAK_SEMPADAN menjadi 50. Layer lalu bernama Sempadan sungai 50 m (baris 77) dan judul peta berbunyi (CONTOH 50 M) (baris 95), karena keduanya memakai %g. Komentar di baris 42 yang menyebut "25 m" tetap, jadi ubah sendiri bila perlu.
  • Teks cetak tetap. Baris 39 mencetak volume > 150 sebagai teks tetap. Bila Anda mengubah ambang di baris 38, teksnya tidak ikut.

Latihan variasi. Lebarkan sempadan menjadi 50 meter, lalu bandingkan luas hasilnya.

Contoh jawaban. Ubah baris 12:

PYTHON
JARAK_SEMPADAN = 50         # meter; angka latihan, bukan ketentuan resmi

Layar menampilkan Luas buffer sungai: 62790 m2 (6.28 ha) dan Total petak dalam sempadan: 51831 m2 (5.18 ha), 10 petak. Jumlah titik panas dalam sempadan tetap 6 dari 12. Layer sempadan di panel bernama Sempadan sungai 50 m, dan judul peta berbunyi (CONTOH 50 M).

Cek paham #

  1. Apa beda penyaji Categorized dan Graduated?
  2. Mengapa Skrip 17.2 menyaring petak kosong sebelum menghitung kelas?
  3. Mengapa Skrip 17.5 aman dijalankan dua kali, padahal dua layout tidak boleh sama namanya?
  4. Apa yang terjadi bila FOLDER di Skrip 17.1 salah, dan baris mana yang menyebabkannya?

Jawaban.

  1. Categorized memberi satu warna untuk tiap nilai teks, misalnya tiap jenis tanaman. Graduated memberi satu warna untuk tiap rentang angka, misalnya tiap kelas volume.
  2. Quantile dan Natural Breaks membaca nilai kosong sebagai nol. Tanpa saringan, kelas pertama ikut tertarik ke nol, dan batas kelas menjadi tidak mencerminkan data.
  3. Baris 14-16 Skrip 17.5 mencari layout lama bernama Peta_KPH_A4 dan membuangnya sebelum layout baru dibuat. Jadi tidak pernah ada dua layout sama nama.
  4. Skrip berhenti dengan AssertionError: Layer tidak terbaca: diikuti nama berkas. Penyebabnya baris 13 (assert layer.isValid()), yang memeriksa bahwa data berhasil dibaca.

Kesalahan umum #

  • Menjalankan skrip tidak berurutan. Skrip 17.2, 17.4, dan 17.5 mengandalkan layer dari skrip sebelumnya. Perbaikan: ikuti tabel urutan di awal bab.
  • Membiarkan jalur contoh. FOLDER menunjuk folder di komputer penulis. Perbaikan: ganti jalur di tiap skrip. Bila muncul AssertionError berpesan "tidak terbaca", periksa jalur dan nama berkas.
  • Menjalankan Skrip 17.7 di proyek yang berisi pekerjaan. Perbaikan: simpan proyek dulu, atau pakai proyek kosong.
  • Mengira hasil ekspor berhasil karena berkas ada. Perbaikan: baca angka hasil. Nilai 0 berarti berhasil.

Ringkasan dan latihan #

  • Peta jadi dirakit bertahap: layer, simbologi, label, layout, ekspor.
  • Categorized mewarnai menurut nilai teks, Graduated menurut rentang angka, dan tiga metode kelas memberi batas berbeda.
  • Layout tersusun dari item yang diletakkan dengan milimeter. Atur ukuran dulu, lalu cakupan, lalu skala.
  • Ekspor PDF tetap tajam. PNG bergantung dpi. Selalu baca angka hasil.
  • Baris assert menghentikan skrip dengan pesan jelas bila data atau layout tidak ada.
  • Dua hal yang perlu diingat: legenda manual hanya mengganti tulisan, bukan nama layer, dan proyek.clear() mengosongkan proyek.

Latihan. Buat salinan Skrip 17.5 dengan layout bernama Peta_Volume_A4, skala 1:3500, dan judul "PETA VOLUME KAYU". Ekspor PDF-nya saja. Bagian mana yang Anda ubah, dan bagian mana yang Anda hapus?

Tabel perbandingan: empat cara menata tampilan peta #

PekerjaanPerkakas di skripIsian utamaSkrip di bab ini
Warna per jenisQgsCategorizedSymbolRendererkolom teks dan kategori17.1
Warna per kelas angkaQgsGraduatedSymbolRendererkolom angka dan metode17.2
Warna rasterQgsSingleBandPseudoColorRendererskala warna dan kelas17.3
Tulisan di petaQgsPalLayerSettingsekspresi dan format teks17.4
Lembar petaQgsPrintLayoutitem, letak, ukuran (mm)17.5 dan 17.7
Berkas siap edarQgsLayoutExporternama layout dan dpi17.6