satupeta-main/app/(modules)/maps/components/geocoding-search/index.tsx
2026-01-27 09:31:12 +07:00

133 lines
3.9 KiB
TypeScript

import { useState } from "react";
import { useAtom } from "jotai";
import { mapAtom } from "../../state/map";
import L from "leaflet";
import { Button } from "@/shared/components/ui/button";
import { Loader2 } from "lucide-react";
import SearchInput from "@/shared/components/search-input";
import { useSetAtom } from "jotai";
import { isOpenMapsetDialogAtom } from "../../state/mapset-dialog";
interface GeocodingResult {
address: string;
location: {
x: number;
y: number;
};
score: number;
}
// Jawa Timur extent in Web Mercator (EPSG:3857)
const JAWA_TIMUR_EXTENT = {
xmin: (111.5 * 20037508.34) / 180, // West boundary
ymin: (-8.5 * 20037508.34) / 180, // South boundary
xmax: (114.5 * 20037508.34) / 180, // East boundary
ymax: (-6.5 * 20037508.34) / 180, // North boundary
spatialReference: { wkid: 102100 },
};
export default function GeocodingSearch() {
const [map] = useAtom(mapAtom);
const [input, setInput] = useState("");
const [locations, setLocations] = useState<GeocodingResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [markers, setMarkers] = useState<L.Marker[]>([]);
const setIsOpenDialog = useSetAtom(isOpenMapsetDialogAtom);
const handleChange = async (val: string) => {
setInput(val);
if (!val.trim()) {
setLocations([]);
return;
}
setIsLoading(true);
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_ARCGIS_GEOCODESERVER}/findAddressCandidates?` +
new URLSearchParams({
f: "json",
singleLine: val,
outFields: "Match_addr,Addr_type",
maxLocations: "5",
searchExtent: JSON.stringify(JAWA_TIMUR_EXTENT),
})
);
const data = await response.json();
setLocations(data.candidates || []);
} catch (error) {
console.error("Error searching address:", error);
setLocations([]);
} finally {
setIsLoading(false);
}
};
const handleLocationSelect = (location: GeocodingResult) => {
if (!map) return;
// Clear existing markers
markers.forEach((marker) => marker.remove());
const newMarkers: L.Marker[] = [];
const marker = L.marker([location.location.y, location.location.x])
.addTo(map)
.bindPopup(location.address);
newMarkers.push(marker);
setMarkers(newMarkers);
// Center map on selected location
map.setView([location.location.y, location.location.x], 15);
// Clear locations list
setLocations([]);
};
return (
<div className="relative">
<div className="relative">
<SearchInput
value={input}
onChange={(val) => handleChange(val)}
placeholder="Cari Lokasi/Dataset"
/>
{isLoading && (
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<Loader2 className="h-4 w-4 animate-spin text-gray-500" />
</div>
)}
</div>
{locations.length > 0 && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg">
<div className="p-2">
<Button
onClick={() => setIsOpenDialog(true)}
className="w-full mb-2"
size="sm"
>
Cari {input ? `"${input}"` : "Data"} di Katalog Mapset
</Button>
<div className="text-sm text-gray-500">
Lokasi ({locations.length})
</div>
</div>
<div className="max-h-60 overflow-y-auto">
{locations.map((location, index) => (
<button
key={index}
className="w-full px-4 py-2 text-left text-sm hover:bg-gray-100 focus:outline-none"
onClick={() => handleLocationSelect(location)}
>
{location.address}
</button>
))}
</div>
</div>
)}
</div>
);
}