import { MapContainer, Popup, TileLayer, useMapEvents, WMSTileLayer } from "react-leaflet"; import { mapConfig } from "../config/map-config"; import { Mapset } from "@/shared/types/mapset"; import { getWmsLayerBounds, parseWmsUrl } from "../utils/wms"; import { LatLngBoundsExpression, LatLngLiteral, LeafletMouseEvent } from "leaflet"; import React, { useEffect, useState } from "react"; import { MapLegend } from "./map-legend"; import { PopupAttribute } from "./popup-attribute"; export default function PreviewMap({ mapset, isActiveControl, centerCustom, onMapClick, isShowPopup = false, isShowLoadingTileLayer = false, isShowLegend = false, }: Readonly<{ mapset: Mapset; isActiveControl?: boolean; centerCustom?: [number, number]; onMapClick?: (e: LeafletMouseEvent) => void; isShowPopup?: boolean; isShowLoadingTileLayer?: boolean; isShowLegend?: boolean; }>) { const parsed = parseWmsUrl(mapset?.layer_url); const [bounds, setBounds] = useState(parsed?.params.bounds ?? undefined); // Popup content state const [popupLoading, setPopupLoading] = useState(false); const [popupError, setPopupError] = useState(null); const [popupData, setPopupData] = useState[] | null>(null); const [clickedPosition, setClickedPosition] = useState(null); // tile layer loading state const [wmsPending, setWmsPending] = useState(0); // jumlah tile yang belum selesai const [wmsBusy, setWmsBusy] = useState(false); // grid loading // Reset popup content when mapset changes useEffect(() => { setClickedPosition(null); setPopupData(null); setPopupError(null); setPopupLoading(false); }, [mapset?.id]); useEffect(() => { const fetchBounds = async () => { if (parsed?.baseUrl && parsed?.params.layers && !parsed?.params.bounds) { const newBounds = await getWmsLayerBounds(parsed.baseUrl, parsed.params.layers); if (newBounds) { setBounds(newBounds); } } }; fetchBounds(); }, [parsed?.baseUrl, parsed?.params.layers, parsed?.params.bounds]); // safety cap: kalau ada tile nyangkut >12s, anggap selesai useEffect(() => { if (wmsPending === 0) return; const t = setTimeout(() => setWmsPending(0), 12000); return () => clearTimeout(t); }, [wmsPending]); const isMapLoading = wmsBusy; // Add a fallback center and zoom const center: [number, number] = centerCustom ?? [mapConfig.center[0], mapConfig.center[1]]; const zoom = 8; return ( {isShowPopup && ( { setClickedPosition(e.latlng); onMapClick?.(e); }} onWmsStart={() => { setPopupError(null); setPopupData(null); setPopupLoading(true); }} onWmsResult={({ data, error }) => { setPopupLoading(false); setPopupError(error ?? null); setPopupData(data ?? null); }} /> )} {parsed && ( setWmsBusy(true), load: () => { setWmsBusy(false); setWmsPending(0); }, }} /> )} {isShowLegend && !isMapLoading && parsed && } {isShowLoadingTileLayer && isMapLoading && (
Memuat peta
)} {clickedPosition && ( {/* loading data state */} {popupLoading &&
Loading…
} {!popupLoading && popupError &&
{popupError}
} {/* succesfully fetched data */} {!popupLoading && !popupError && popupData && } {!popupLoading && !popupError && popupData !== undefined && !popupData?.length &&
No data available.
}
)} ); } // Local helper: register Leaflet click events inside MapContainer function MapClick({ onClick, onWmsStart, onWmsResult, parsed, }: { onClick?: (e: LeafletMouseEvent) => void; onWmsStart?: () => void; // ⬇️ "data" untuk kirim objek JSON onWmsResult?: (r: { data?: Record[]; html?: string; error?: string; url?: string }) => void; parsed: ReturnType | null | undefined; }) { useMapEvents({ click: async (e) => { onClick?.(e); onWmsStart?.(); try { if (!parsed?.baseUrl || !parsed?.params?.layers) { onWmsResult?.({ error: "WMS baseUrl/layers tidak tersedia" }); return; } const map = e.target; const version = parsed.params.version || "1.1.1"; const srs = parsed.params.srs || "EPSG:4326"; const size = map.getSize(); const bounds = map.getBounds(); const sw = bounds.getSouthWest(); const ne = bounds.getNorthEast(); const pt = map.latLngToContainerPoint(e.latlng); const X = Math.round(pt.x); const Y = Math.round(pt.y); const urlObj = new URL(parsed.baseUrl); urlObj.searchParams.set("SERVICE", "WMS"); urlObj.searchParams.set("VERSION", version); urlObj.searchParams.set("REQUEST", "GetFeatureInfo"); urlObj.searchParams.set("FORMAT", "image/png"); urlObj.searchParams.set("TRANSPARENT", "true"); urlObj.searchParams.set("QUERY_LAYERS", parsed.params.layers); urlObj.searchParams.set("LAYERS", parsed.params.layers); urlObj.searchParams.set("STYLES", parsed.params.styles ?? ""); urlObj.searchParams.set("exceptions", "application/vnd.ogc.se_inimage"); // ⬇️ minta JSON dulu (kalau didukung GeoServer) urlObj.searchParams.set("INFO_FORMAT", "application/json"); urlObj.searchParams.set("FEATURE_COUNT", "50"); if (version === "1.3.0") { urlObj.searchParams.set("CRS", srs); } else { urlObj.searchParams.set("SRS", srs); } // BBOX lon,lat untuk 1.1.1 EPSG:4326 urlObj.searchParams.set("BBOX", [sw.lng, sw.lat, ne.lng, ne.lat].join(",")); urlObj.searchParams.set("WIDTH", String(size.x)); urlObj.searchParams.set("HEIGHT", String(size.y)); if (version === "1.3.0") { urlObj.searchParams.set("I", String(X)); urlObj.searchParams.set("J", String(Y)); } else { urlObj.searchParams.set("X", String(X)); urlObj.searchParams.set("Y", String(Y)); } const url = urlObj.toString(); // --- fetch: prefer JSON, fallback HTML --- const res = await fetch(url, { headers: { Accept: "application/json, text/html;q=0.9" }, }); const raw = await res.text(); if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); let features: Record[] = []; const ctype = res.headers.get("content-type") || ""; const looksJson = ctype.includes("application/json") || /^[\[\{]/.test(raw.trim()); if (looksJson) { // JSON dari GeoServer (bentuk bisa variasi) const j = JSON.parse(raw); if (Array.isArray(j)) { features = j as Record[]; } else if (Array.isArray(j?.features)) { // GeoJSON-like features = j.features.map((f: unknown) => (typeof f === "object" && f !== null && "properties" in f ? (f as { properties: unknown }).properties : f)); } else if (j?.properties) { features = [j.properties]; } else { features = [j]; } } else { // Fallback: parse HTML tabel → array objek const doc = new DOMParser().parseFromString(raw, "text/html"); const tables = Array.from(doc.querySelectorAll("table")); for (const tbl of tables) { const rows = Array.from((tbl as HTMLTableElement).rows); if (!rows.length) continue; const headerRow = rows.find((r) => Array.from(r.cells).every((c) => c.tagName === "TH")) || rows[0]; const headers = Array.from(headerRow.cells).map((c) => (c.textContent || "").trim()); const start = rows.indexOf(headerRow) + 1; for (let i = start; i < rows.length; i++) { const r = rows[i]; const obj: Record = {}; headers.forEach((h, idx) => { if (!h) return; obj[h] = r.cells[idx]?.textContent?.trim() ?? ""; }); if (Object.values(obj).some((v) => v !== "")) features.push(obj); } } } // kirim objek JSON (array of row objects) onWmsResult?.({ data: features, url }); } catch (err: unknown) { onWmsResult?.({ error: err instanceof Error ? err.message : "Gagal memuat GetFeatureInfo", }); } }, }); return null; }