Titik lokasi saja belum menggambarkan bisnis toko buku kita sepenuhnya: buku berjalan dari gudang ke toko lewat rute pengiriman, dan setiap toko melayani area cakupan tertentu — bukan sekadar titik di peta. GeoJSON menyediakan tipe geometri untuk semuanya, sekaligus menjadi format standar yang sama di Google Maps, Leaflet, dan PostGIS. Chapter ini membedah tipe-tipe geometrinya, aturan ketat polygon, dan eksperimen validasi yang membuktikan seberapa awas MongoDB menolak data koordinat yang keliru.

Peta materi Chapter ini:

  • Tipe geometri GeoJSON: LineString, Polygon, hingga MultiPolygon.
  • Aturan polygon: ring tertutup, ring luar, dan lubang.
  • Validasi otomatis saat insert — termasuk jebakan urutan longitude/latitude.

Rute, Area, dan Beberapa Area Sekaligus #

Kita lengkapi latihan_geo dengan tiga collection baru: rute untuk jalur pengiriman (LineString), area untuk cakupan layanan (Polygon), dan area_ganda untuk toko yang melayani dua kawasan terpisah (MultiPolygon). Setiap field geometri langsung kita beri index 2dsphere — alat yang akan dibedah penuh di Chapter Geospatial Indexes:

js
use latihan_geo
db.rute.drop()
db.area.drop()
db.area_ganda.drop()
db.rute.insertOne({
  nama: "Rute Gudang Pasar Baru - Monas",
  jalur: { type: "LineString", coordinates: [ [106.822, -6.160], [106.826, -6.166], [106.828, -6.171], [106.827, -6.175] ] }
})
db.area.insertOne({
  nama: "Area Menteng", toko: "Toko Menteng",
  cakupan: {
    type: "Polygon",
    coordinates: [
      [ [106.80, -6.22], [106.87, -6.22], [106.87, -6.15], [106.80, -6.15], [106.80, -6.22] ],
      [ [106.82, -6.20], [106.84, -6.20], [106.84, -6.18], [106.82, -6.18], [106.82, -6.20] ]
    ]
  }
})
db.area_ganda.insertOne({
  nama: "Area Ganda Menteng-Dago", toko: "Toko Menteng",
  cakupan: {
    type: "MultiPolygon",
    coordinates: [
      [ [ [106.80, -6.22], [106.87, -6.22], [106.87, -6.15], [106.80, -6.15], [106.80, -6.22] ] ],
      [ [ [107.55, -6.95], [107.70, -6.95], [107.70, -6.85], [107.55, -6.85], [107.55, -6.95] ] ]
    ]
  }
})
db.rute.createIndex({ jalur: "2dsphere" })
db.area.createIndex({ cakupan: "2dsphere" })
db.area_ganda.createIndex({ cakupan: "2dsphere" })
db.rute.find({}, { _id: 0, nama: 1, jalur: 1 })

Output yang Dihasilkan:

text
switched to db latihan_geo
true
true
true
{
  acknowledged: true,
  insertedId: ObjectId('6ad3e1c7ed5597a65a1bca28')
}
{
  acknowledged: true,
  insertedId: ObjectId('6ad3e1c7ed5597a65a1bca29')
}
{
  acknowledged: true,
  insertedId: ObjectId('6ad3e1c7ed5597a65a1bca2a')
}
jalur_2dsphere
cakupan_2dsphere
cakupan_2dsphere
[
  {
    nama: 'Rute Gudang Pasar Baru - Monas',
    jalur: {
      type: 'LineString',
      coordinates: [
        [ 106.822, -6.16 ],
        [ 106.826, -6.166 ],
        [ 106.828, -6.171 ],
        [ 106.827, -6.175 ]
      ]
    }
  }
]

Struktur ketiga tipe itu membangun satu sama lain:

  1. LineString: coordinates berisi array titik [lng, lat] berurutan — jalur pengiriman dari Pasar Baru menuju Monas. Minimal dua titik.
  2. Polygon: coordinates berisi array ring — dan setiap ring adalah LineString tertutup. Ring pertama adalah batas luar; ring kedua dan seterusnya adalah lubang (materi bagian akhir chapter ini).
  3. MultiPolygon: array dari beberapa polygon lengkap — dipakai saat satu entitas melayani kawasan yang terputus, seperti Menteng di Jakarta dan Dago di Bandung.

Aturan Ring Tertutup #

Setiap ring polygon wajib tertutup: titik pertama dan titik terakhir harus identik, dan jumlah titiknya minimal empat. Ring yang dibiarkan terbuka ditolak mentah-mentah saat insert — asalkan field itu telah ber-index 2dsphere:

js
try {
  db.area.insertOne({
    nama: "Zona Uji", toko: "Toko Menteng",
    cakupan: { type: "Polygon", coordinates: [ [ [106.80, -6.22], [106.87, -6.22], [106.87, -6.15], [106.80, -6.15] ] ] }
  })
} catch (e) {
  print("Galat: " + e.code + " :: " + e.message.split(":: caused by ::").pop().trim())
}

Output yang Dihasilkan:

text
Galat: 510 :: Could not extract geo keys at path 'cakupan' for document with _id ObjectId('6ad3e1c8ed5597a65a1bca2b'): Loop is not closed, first vertex does not equal last vertex: [ [ 106.8, -6.22 ], [ 106.87, -6.22 ], [ 106.87, -6.15 ], [ 106.8, -6.15 ] ]

Error 510 menuliskan kesalahannya dengan sangat eksplisit: Loop is not closed, first vertex does not equal last vertex, disertai ring yang bermasalah. Bandingkan dengan ring yang sukses di block sebelumnya — titik [106.80, -6.22] muncul dua kali, di awal dan di akhir. Aturan lengkap ring-nya:

  • Ring tertutup: titik awal = titik akhir.
  • Minimal empat pasang koordinat (tiga sudut nyata + satu pengulangan penutup).
  • Ring tunggal tidak boleh memotong dirinya sendiri (self-intersecting).

Lubang di Dalam Polygon #

Kembali ke document Area Menteng yang tersimpan di db.area: coordinates-nya memuat dua ring. Ring pertama adalah batas luar kawasan; ring kedua [106.82, -6.20] ... [106.82, -6.20] adalah lubang — kawasan kecil di dalam batas luar yang tidak termasuk cakupan, misalnya kompleks yang dilayani kurir lain. Titik yang jatuh di lubang dianggap berada di luar polygon.

Validasi Koordinat dan Jebakan Urutan #

Sekarang bagian yang paling sering menghabiskan waktu debugging: koordinat di luar rentang. Percobaan berikut sengaja berurutan — pertama menyisipkan koordinat bogus sebelum ada index, lalu mencoba membuat index, lalu menyisipkan lagi setelah index ada:

js
db.toko.insertOne({ nama: "Toko Uji", lokasi: { type: "Point", coordinates: [999, -6.2] } })
try {
  db.toko.createIndex({ lokasi: "2dsphere" })
} catch (e) {
  print("Galat saat build index: " + e.message.split(":: caused by ::").pop().trim())
}
db.toko.deleteOne({ nama: "Toko Uji" })
db.toko.createIndex({ lokasi: "2dsphere" })
try {
  db.toko.insertOne({ nama: "Toko Uji", lokasi: { type: "Point", coordinates: [999, -6.2] } })
} catch (e) {
  print("Galat: " + e.code + " :: " + e.message.split(":: caused by ::").pop().trim())
}
try {
  db.toko.insertOne({ nama: "Toko Uji", lokasi: { type: "Point", coordinates: [-6.2, 106.8] } })
} catch (e) {
  print("Galat: " + e.code + " :: " + e.message.split(":: caused by ::").pop().trim())
}

Output yang Dihasilkan:

text
{
  acknowledged: true,
  insertedId: ObjectId('6ad3e1c8ed5597a65a1bca2c')
}
Galat saat build index: Could not extract geo keys at path 'lokasi' for document with _id ObjectId('6ad3e1c8ed5597a65a1bca2c'): Longitude/latitude is out of bounds, lng: 999 lat: -6.2

{ acknowledged: true, deletedCount: 1 }
lokasi_2dsphere
Galat: 510 :: Could not extract geo keys at path 'lokasi' for document with _id ObjectId('6ad3e1c8ed5597a65a1bca2d'): Longitude/latitude is out of bounds, lng: 999 lat: -6.2

Galat: 510 :: Could not extract geo keys at path 'lokasi' for document with _id ObjectId('6ad3e1c8ed5597a65a1bca2e'): Longitude/latitude is out of bounds, lng: -6.2 lat: 106.8

Urutan kejadian di atas mengandung tiga pelajaran:

  1. Insert pertama diterima — [999, -6.2] masuk tanpa protes karena validasi GeoJSON di MongoDB hanya aktif ketika field tersebut di-index. Data kotor bisa hidup lama sebelum ketahuan.
  2. Pembuatan index jadi yang menanggung — createIndex memindai seluruh collection dan gagal di document kotor itu. Membersihkan data lalu membuat ulang index adalah satu-satunya jalan; begitu pula yang dilakukan baris deleteOne di atas.
  3. Setelah index aktif, dua jebakan tertangkap — longitude 999 tertolak langsung, dan yang lebih penting: koordinat Jakarta yang tertukar [-6.2, 106.8] tertangkap karena latitude 106.8 melebihi 90. Pesan errornya bahkan menuliskan kedua nilai yang diperiksa: lng: -6.2 lat: 106.8.

Rangkuman #

  • GeoJSON bertingkat: titik menjadi LineString (rute), ring tertutup menjadi Polygon (area), dan kumpulan polygon menjadi MultiPolygon.
  • Ring polygon wajib tertutup (titik awal = akhir), minimal empat titik, dan tidak boleh memotong dirinya sendiri; error 510 menunjuk ring yang bermasalah.
  • Ring kedua dan seterusnya adalah lubang — area di dalamnya dianggap di luar cakupan.
  • Validasi GeoJSON hanya berjalan pada field yang ber-index 2dsphere; tanpa index, data tak valid tersimpan dan baru merusak saat index dibuat.
  • Koordinat tertukar [-6.2, 106.8] tertangkap untuk kota-kota Indonesia karena latitude-nya melebihi 90.

Data sudah rapi dan tervalidasi. Chapter berikutnya membahas alat yang membuatnya semua cepat: index geospatial 2d dan 2dsphere.