satupeta-main/app/(modules)/maps/components/sidebar/layer-controls/layer-control-item/index.tsx
2026-02-23 12:21:05 +07:00

146 lines
5.5 KiB
TypeScript
Executable File

import { useState } from "react";
import { GripHorizontal, ChevronUp, Eye, EyeOff } from "lucide-react";
import L from "leaflet";
import { useAtom } from "jotai";
import { isOpenMapsetDialogAtom, selectedMapsetAtom } from "../../../../state/mapset-dialog";
import { useQuery } from "@tanstack/react-query";
import { ActiveLayer, removeLayerAtom, toggleLayerAtom, setLayerOpacityAtom } from "../../../../state/active-layers";
import { mapAtom } from "../../../../state/map";
import mapsetApi from "@/shared/services/mapset";
import { LegendDisplay } from "./legend-display";
import { OpacityControl } from "./opacity-control";
import { LayerActions } from "./layer-actions";
import dynamic from "next/dynamic";
const ChoroplethControl = dynamic(() => import("./choropleth-control"), {
ssr: false,
loading: () => <div className="w-full aspect-square bg-gray-200 animate-pulse" />,
});
interface LayerControlItemProps {
layer: ActiveLayer;
layerInstance?: L.Layer;
onZoom: (bounds?: L.LatLngBoundsExpression | null) => void;
}
export const LayerControlItem = ({ layer, layerInstance, onZoom }: LayerControlItemProps) => {
const [isExpanded, setIsExpanded] = useState(false);
// Drive slider from global state so newly created instances pick it up
const opacity = layer.settings.opacity;
const [, setIsOpenDialog] = useAtom(isOpenMapsetDialogAtom);
const [, setSelectedMapset] = useAtom(selectedMapsetAtom);
const [, removeLayer] = useAtom(removeLayerAtom);
const [, toggleLayer] = useAtom(toggleLayerAtom);
const [, setLayerOpacity] = useAtom(setLayerOpacityAtom);
const { data: mapset } = useQuery({
queryKey: ["mapset", layer.source.id],
queryFn: () =>
mapsetApi.getMapsetById(layer.source.id.toString(), { skipAuth: true }).then((res) => {
return res;
}),
});
const [map] = useAtom(mapAtom);
const handleOpacityChange = (value: number[]) => {
const next = value[0];
// Update the Leaflet instance immediately for responsiveness
if (layerInstance && layerInstance instanceof L.TileLayer.WMS) {
layerInstance.setOpacity(next);
}
if (layerInstance && layerInstance instanceof L.GeoJSON) {
layerInstance.setStyle({
fillOpacity: next,
opacity: next,
});
}
// Persist in global state so new/reattached instances get correct opacity
setLayerOpacity({ layerId: layer.id, opacity: next });
};
const handleInfo = () => {
if (!mapset) return;
setSelectedMapset(mapset);
setIsOpenDialog(true);
};
const handleToggleVisibility = () => {
if (!map) return;
toggleLayer(layer.id);
if (layerInstance) {
if (layer.settings.visible) {
layerInstance.remove();
} else {
layerInstance.addTo(map);
}
}
};
function toLatLngBounds(bounds: L.LatLngBoundsExpression): L.LatLngBounds {
if ("getSouthWest" in bounds && "getNorthEast" in bounds) {
return bounds as L.LatLngBounds;
}
return L.latLngBounds(bounds as L.LatLngExpression[]);
}
const handleDownloadImage = () => {
if (!layer.layer.url || !layer.layer.layers) return;
const rawBounds = layer.layer.bounds ?? map?.getBounds() ?? null;
if (!rawBounds) return;
const bounds = toLatLngBounds(rawBounds);
const southWest = bounds.getSouthWest();
const northEast = bounds.getNorthEast();
const bbox = [southWest.lng, southWest.lat, northEast.lng, northEast.lat].join(",");
const width = 1024;
const height = 768;
const downloadUrl = `${layer.layer.url}?service=WMS&version=1.1.1&request=GetMap&layers=${layer.layer.layers}&styles=&bbox=${bbox}&width=${width}&height=${height}&srs=EPSG:4326&format=image/png&transparent=true`;
window.open(downloadUrl, "_blank");
};
return (
<div className="bg-muted rounded-lg border border-primary p-3">
<div className="cursor-pointer w-full flex items-center justify-between text-sm">
<div className="flex flex-1 items-center gap-2">
<button className="cursor-pointer self-start mt-0.5">
<GripHorizontal className="w-4 h-4 text-zinc-950"></GripHorizontal>
</button>
<button onClick={() => setIsExpanded((prev) => !prev)} className={`text-left text-zinc-700 text-sm cursor-pointer ${!isExpanded && "line-clamp-1"}`}>
{layer.name}
</button>
</div>
{!isExpanded && (
<button onClick={handleToggleVisibility} className="cursor-pointer flex items-center justify-center" title={layer.settings.visible ? "Hide layer" : "Show layer"}>
{layer.settings.visible ? <Eye className="h-4 w-4 text-zinc-950" /> : <EyeOff className="h-4 w-4 text-zinc-950" />}
</button>
)}
{!isExpanded && <div className="border-l border-gray-300 h-4 mx-2"></div>}
<button onClick={() => setIsExpanded((prev) => !prev)} className="flex items-center justify-center shrink-0 self-start mt-0.5">
{isExpanded ? <ChevronUp size={12} className="h-4 w-4 rotate-0 shrink-0 text-zinc-950" /> : <ChevronUp size={12} className="rotate-180 h-4 w-4 text-zinc-950" />}
</button>
</div>
{isExpanded && (
<div className="space-y-3 mt-3">
<LegendDisplay layer={layer} />
<LayerActions layer={layer} onZoom={onZoom} onInfo={handleInfo} onRemove={() => removeLayer(layer.id)} onDownload={handleDownloadImage} />
<OpacityControl opacity={opacity} onOpacityChange={handleOpacityChange} />
{mapset?.layer_type === "point" && <ChoroplethControl layer={layer} />}
</div>
)}
</div>
);
};