satupeta-main/app/(modules)/maps/components/sidebar/drawing-tools/index.tsx

276 lines
7.8 KiB
TypeScript
Raw Normal View History

2026-01-27 02:31:12 +00:00
"use client";
import { useAtom } from "jotai";
import { MapPinIcon, SlashIcon, Scale3DIcon } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { mapAtom } from "../../../state/map";
import L, { DrawMap } from "leaflet";
import "leaflet-draw";
import "leaflet-draw/dist/leaflet.draw.css";
import * as turf from "@turf/turf";
export default function DrawingTools() {
const [map] = useAtom(mapAtom);
const [activeTab, setActiveTab] = useState<
"location" | "edit" | "expand" | null
>(null);
// Refs untuk menyimpan instance drawing tools dan layer group
const drawnItemsRef = useRef<L.FeatureGroup | null>(null);
const drawingToolsRef = useRef<{
marker: L.Draw.Marker | null;
polyline: L.Draw.Polyline | null;
polygon: L.Draw.Polygon | null;
}>({
marker: null,
polyline: null,
polygon: null,
});
// Inisialisasi layer group dan events
useEffect(() => {
if (!map) return;
// Buat FeatureGroup untuk items yang digambar
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
drawnItemsRef.current = drawnItems;
// Inisialisasi drawing tools
drawingToolsRef.current = {
marker: new L.Draw.Marker(map as DrawMap, {
icon: new L.Icon.Default(),
}),
polyline: new L.Draw.Polyline(map as DrawMap, {
shapeOptions: {
color: "#3388ff",
weight: 4,
opacity: 0.7,
},
}),
polygon: new L.Draw.Polygon(map as DrawMap, {
shapeOptions: {
color: "#f03",
fillColor: "#f03",
fillOpacity: 0.3,
weight: 3,
},
showArea: true,
}),
};
// Menangani event ketika objek selesai digambar
// eslint-disable-next-line @typescript-eslint/no-explicit-any
map.on(L.Draw.Event.CREATED, function (e: any) {
const layer = e.layer;
// Tambahkan layer ke drawnItems
drawnItems.addLayer(layer);
// Tambahkan popup informasi berdasarkan tipe layer
if (layer instanceof L.Marker) {
addMarkerPopup(layer);
} else if (layer instanceof L.Polyline && !(layer instanceof L.Polygon)) {
addPolylinePopup(layer);
} else if (layer instanceof L.Polygon) {
addPolygonPopup(layer);
}
});
// Cleanup event listeners ketika component unmount
return () => {
if (map) {
map.off(L.Draw.Event.CREATED);
if (drawnItems) {
map.removeLayer(drawnItems);
}
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map]);
// Function to add a popup to a marker
const addMarkerPopup = (marker: L.Marker) => {
const latlng = marker.getLatLng();
marker.bindPopup(`
<div class="popup-content">
<strong>Lokasi:</strong><br>
Lat: ${latlng.lat.toFixed(6)}<br>
Lng: ${latlng.lng.toFixed(6)}
</div>
`);
};
// Function to add a popup to a polyline
const addPolylinePopup = (polyline: L.Polyline) => {
const latlngs = polyline.getLatLngs() as L.LatLng[];
const distance = calculateDistance(latlngs);
polyline.bindPopup(`
<div class="popup-content">
<strong>Garis:</strong><br>
Jarak: ${formatDistance(distance)}<br>
</div>
`);
};
// Function to add a popup to a polygon
const addPolygonPopup = (polygon: L.Polygon) => {
const latlngs = polygon.getLatLngs()[0] as L.LatLng[];
const area = calculateArea(latlngs);
polygon.bindPopup(`
<div class="popup-content">
<strong>Area:</strong><br>
Luas: ${formatArea(area)}<br>
</div>
`);
};
// Hitung jarak polyline dalam meter dengan Turf
const calculateDistance = (latlngs: L.LatLng[]): number => {
if (latlngs.length < 2) return 0;
const coords = latlngs.map((p) => [p.lng, p.lat]);
const line = turf.lineString(coords);
return turf.length(line, { units: "kilometers" }) * 1000; // dalam meter
};
// Hitung luas polygon dalam meter persegi dengan Turf
const calculateArea = (latlngs: L.LatLng[]): number => {
if (latlngs.length < 3) return 0;
const coords = latlngs.map((p) => [p.lng, p.lat]);
// Tutup polygon jika belum tertutup
if (
coords[0][0] !== coords[coords.length - 1][0] ||
coords[0][1] !== coords[coords.length - 1][1]
) {
coords.push(coords[0]);
}
const polygon = turf.polygon([coords]);
return turf.area(polygon); // m²
};
// Utility function to format distances
const formatDistance = (distance: number): string => {
if (distance >= 1000) {
// Convert to kilometers, format with commas, and ensure two decimal places
return `${new Intl.NumberFormat("id-ID").format(
parseFloat((distance / 1000).toFixed(2))
)} km`;
}
// Format in meters with commas and ensure two decimal places
return `${new Intl.NumberFormat("id-ID").format(
parseFloat(distance.toFixed(2))
)} meter`;
};
// Utility function to format areas
const formatArea = (area: number): string => {
if (area >= 1000000) {
// Convert to square kilometers, format with commas, and ensure two decimal places
return `${new Intl.NumberFormat("id-ID").format(
parseFloat((area / 1000000).toFixed(2))
)} km²`;
}
// Format in square meters with commas and ensure two decimal places
return `${new Intl.NumberFormat("id-ID").format(
parseFloat(area.toFixed(2))
)} m²`;
};
// Handler untuk tab click
const handleTabClick = (tab: "location" | "edit" | "expand") => {
// Disable all drawing tools first
if (drawingToolsRef.current) {
drawingToolsRef.current.marker?.disable();
drawingToolsRef.current.polyline?.disable();
drawingToolsRef.current.polygon?.disable();
}
// Toggle active tab
setActiveTab((prevTab) => {
if (prevTab === tab) {
return null;
}
return tab;
});
// If tab is deactivated, don't enable any drawing tool
if (activeTab === tab) {
return;
}
// Enable the selected drawing tool
if (drawingToolsRef.current) {
switch (tab) {
case "location":
drawingToolsRef.current.marker?.enable();
break;
case "edit":
drawingToolsRef.current.polyline?.enable();
break;
case "expand":
drawingToolsRef.current.polygon?.enable();
break;
}
}
};
// Reset semua item yang digambar
const handleReset = () => {
setActiveTab(null);
// Hapus semua layer
if (drawnItemsRef.current) {
drawnItemsRef.current.clearLayers();
}
};
return (
<div className="flex flex-col gap-3 w-full max-w-md">
<div className="grid grid-cols-3 bg-zinc-100 rounded-lg p-1">
<button
className={`flex items-center justify-center py-1 ${
activeTab === "location" ? "bg-zinc-300" : ""
}`}
onClick={() => handleTabClick("location")}
aria-label="Location"
>
<MapPinIcon className="w-4 h-4 text-zinc-500" />
</button>
<button
className={`flex items-center justify-center py-1 ${
activeTab === "edit" ? "bg-zinc-300" : ""
}`}
onClick={() => handleTabClick("edit")}
aria-label="Edit"
>
<SlashIcon className="w-4 h-4 text-zinc-500" />
</button>
<button
className={`flex items-center justify-center py-1 ${
activeTab === "expand" ? "bg-zinc-300" : ""
}`}
onClick={() => handleTabClick("expand")}
aria-label="Expand"
>
<Scale3DIcon className="w-4 h-4 text-zinc-500" />
</button>
</div>
<button
className="w-full bg-zinc-100 rounded-lg py-2 px-4 text-zinc-600 hover:bg-zinc-200 transition-colors"
onClick={handleReset}
>
Reset
</button>
</div>
);
}