276 lines
7.8 KiB
TypeScript
276 lines
7.8 KiB
TypeScript
"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>
|
|
);
|
|
}
|