geospatial-maps-expert · v4.3.0 · 2026-10-20 · sha256 3d3bc8107fafbb45

geospatial-maps-expert v4.3.0A

Immutable. This exact content never changes and is served at /api/v1/blob/3d3bc8107fafbb45.

---
name: geospatial-maps-expert
description: "Expert guide for maps and geospatial data (Mapbox GL JS, Leaflet, Google Maps, PostGIS) / Panduan ahli peta dan data geospasial (Mapbox GL JS, Leaflet, Google Maps, PostGIS)."
author: "Roedy Rustam"
version: "4.3.0"
---

# Geospatial & Maps Expert (2026 Edition)

[English](#english) | [Bahasa Indonesia](#bahasa-indonesia)

---

<a name="english"></a>
## English

### Description
Expert guide for integrating interactive mapping engines, geospatial vector tiles, and spatial database queries into modern web and mobile applications. Covers MapLibre GL JS (open-source, license-free), Mapbox GL JS v3, Leaflet, Google Maps Platform, deck.gl large-scale visualization, Supercluster client-side marker aggregation, and PostgreSQL/PostGIS spatial query optimization.

### Trigger Conditions
- Embedding interactive vector maps into React, Next.js, or mobile applications.
- Implementing location-based search (*"Find outlets within 5 km"*).
- Managing thousands of map markers with client-side clustering.
- Writing spatial SQL queries using PostGIS (`ST_DWithin`, `ST_MakePoint`, `ST_Distance`).
- Visualizing geospatial GeoJSON datasets, routes, and heatmaps.

### PostGIS Spatial Query Pattern (PostgreSQL)

```sql
-- Enable PostGIS extension and configure geometry table
CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TABLE IF NOT EXISTS merchant_locations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name VARCHAR(255) NOT NULL,
  address TEXT,
  geom GEOMETRY(Point, 4326) NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Crucial: GIST Spatial Index for sub-millisecond radius search
CREATE INDEX IF NOT EXISTS idx_merchant_geom ON merchant_locations USING GIST (geom);

-- Find nearest merchants within 5,000 meters (5 km) of user's coordinates
-- Note: Cast to GEOGRAPHY to calculate true geodesic distance in meters
SELECT 
  id, 
  name, 
  address,
  ST_Y(geom::geometry) AS latitude,
  ST_X(geom::geometry) AS longitude,
  ROUND(ST_Distance(geom::geography, ST_SetSRID(ST_MakePoint(:userLng, :userLat), 4326)::geography)::numeric, 1) AS distance_meters
FROM merchant_locations
WHERE ST_DWithin(geom::geography, ST_SetSRID(ST_MakePoint(:userLng, :userLat), 4326)::geography, 5000)
ORDER BY distance_meters ASC
LIMIT 20;
```

### High-Performance React Map Component (MapLibre GL JS)

```tsx
// src/components/maps/StoreLocatorMap.tsx
'use client';

import { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';

interface StorePoint {
  id: string;
  name: string;
  lng: number;
  lat: number;
}

export function StoreLocatorMap({ stores }: { stores: StorePoint[] }) {
  const mapContainer = useRef<HTMLDivElement>(null);
  const mapRef = useRef<maplibregl.Map | null>(null);

  useEffect(() => {
    if (!mapContainer.current) return;

    const map = new maplibregl.Map({
      container: mapContainer.current,
      style: 'https://tiles.openfreemap.org/styles/liberty', // Free open-source vector tiles
      center: [106.8456, -6.2088], // Jakarta default
      zoom: 12,
    });

    map.addControl(new maplibregl.NavigationControl(), 'top-right');

    map.on('load', () => {
      // Add GeoJSON source with built-in Supercluster support
      map.addSource('stores', {
        type: 'geojson',
        data: {
          type: 'FeatureCollection',
          features: stores.map((s) => ({
            type: 'Feature',
            geometry: { type: 'Point', coordinates: [s.lng, s.lat] },
            properties: { id: s.id, name: s.name },
          })),
        },
        cluster: true,
        clusterMaxZoom: 14,
        clusterRadius: 50,
      });

      // Cluster circle layer
      map.addLayer({
        id: 'clusters',
        type: 'circle',
        source: 'stores',
        filter: ['has', 'point_count'],
        paint: {
          'circle-color': '#059669',
          'circle-radius': ['step', ['get', 'point_count'], 18, 10, 24, 50, 30],
        },
      });

      // Unclustered point marker
      map.addLayer({
        id: 'unclustered-point',
        type: 'circle',
        source: 'stores',
        filter: ['!', ['has', 'point_count']],
        paint: {
          'circle-color': '#2563eb',
          'circle-radius': 8,
          'circle-stroke-width': 2,
          'circle-stroke-color': '#ffffff',
        },
      });
    });

    mapRef.current = map;
    return () => map.remove();
  }, [stores]);

  return <div ref={mapContainer} className="h-[500px] w-full rounded-xl border border-slate-200 overflow-hidden" />;
}
```

## Orchestration & Integration
- **`database-orm-expert`**: Bridges PostGIS spatial types with Prisma / Drizzle ORM schemas.
- **`senior-frontend`**: Integrates responsive map canvases without blocking main-thread UI interactivity.
- **`performance-web-vitals`**: Lazy-loads vector tile stylesheets and WebGL libraries to maintain optimal LCP and INP.

---

<a name="bahasa-indonesia"></a>
## Bahasa Indonesia

### Deskripsi
Panduan ahli untuk mengintegrasikan mesin peta interaktif, ubin vektor (*vector tiles*), dan kueri database geospasial ke dalam aplikasi web dan mobile modern. Mencakup MapLibre GL JS (bebas lisensi dan *open-source*), Mapbox GL JS v3, Leaflet, Google Maps Platform, visualisasi data berskala besar dengan deck.gl, kluster marker sisi klien dengan Supercluster, serta optimasi kueri spasial PostgreSQL/PostGIS.

### Kondisi Pemicu
- Menambahkan peta vektor interaktif ke dalam komponen React atau Next.js.
- Mengimplementasikan pencarian berbasis radius jarak (*"Cari gerai terdekat dalam radius 5 km"*).
- Menampilkan ribuan titik marker secara efisien menggunakan kluster otomatis.
- Menulis kueri geospasial PostGIS (`ST_DWithin`, `ST_Distance`, `ST_MakePoint`).
- Visualisasi GeoJSON, rute navigasi, dan peta kepadatan (*heatmap*).

### Pola Integrasi Kueri Spasial di Backend (TypeScript + Drizzle/Raw SQL)

```typescript
// Pencarian gerai terdekat menggunakan PostGIS
export async function getNearbyStores(userLat: number, userLng: number, radiusMeters = 5000) {
  const query = `
    SELECT id, name, address,
           ST_Y(geom) as lat, ST_X(geom) as lng,
           ROUND(ST_Distance(geom::geography, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography)::numeric, 1) as distance
    FROM merchant_locations
    WHERE ST_DWithin(geom::geography, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3)
    ORDER BY distance ASC
    LIMIT 20;
  `;
  const result = await db.execute(query, [userLng, userLat, radiusMeters]);
  return result.rows;
}
```

### Praktik Terbaik Kinerja Peta
1. **Gunakan Indeks GIST**: Kueri spasial tanpa indeks GIST akan memindai seluruh tabel secara sekuensial. Wajib pasang `CREATE INDEX ... USING GIST (geom)`.
2. **Ubah ke Tipe GEOGRAPHY**: Untuk menghitung jarak nyata di bumi dalam meter/kilometer, lakukan *casting* `geom::geography`.
3. **Kluster di Sisi Klien**: Jangan render ribuan node DOM untuk setiap marker. Gunakan fitur clustering WebGL bawaan MapLibre agar performa rendering tetap 60 FPS.

## Integrasi Orkestrasi
- **`database-orm-expert`**: Mengelola migrasi tabel dengan tipe data geospasial PostGIS.
- **`senior-frontend`**: Menangani state interaksi klik marker dan jendela pop-up.
- **`performance-web-vitals`**: Menerapkan dynamic import (`next/dynamic` dengan `ssr: false`) agar bundle peta tidak memperlambat pemuatan awal.