246 lines
6.9 KiB
TypeScript
246 lines
6.9 KiB
TypeScript
/**
|
|
* ➡️ 경로 : 하수관망도 메인페이지 내 침수심 범례
|
|
*
|
|
* 📄 WaterDepth Component
|
|
* 침수심 범례 컴포넌트
|
|
*
|
|
* - 침수심 레이어의 범례 표시 및 애니메이션 재생 제어
|
|
* - 진행률 바를 통한 애니메이션 상태 시각화
|
|
* - 플레이/일시정지 버튼을 통한 사용자 상호작용
|
|
* - 범례 항목별 색상과 레이블 표시
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { Label } from "@common/components/ui/label";
|
|
import { useEffect } from "react";
|
|
|
|
export const WATER_DEPTH_LAYER_TYPES: string[] = [
|
|
"tb_ms_flood_dngr_map", // 홍수위험지도
|
|
"tb_ms_sbmrg_dngr_map", // 침수위험지도
|
|
];
|
|
|
|
export type WaterDepthLegendItem = {
|
|
key: string;
|
|
label: string;
|
|
color: string;
|
|
borderColor: string;
|
|
sortSeq: number;
|
|
};
|
|
|
|
const getLayerTypeName = (layerInfo: any) =>
|
|
String(
|
|
layerInfo?.lyrTyNm ?? layerInfo?.lyrTypeNm ?? layerInfo?.lyr_type_nm ?? "",
|
|
).trim();
|
|
|
|
const getFiniteSortOrder = (value: unknown) => {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : Number.MAX_SAFE_INTEGER;
|
|
};
|
|
|
|
const getLegendNumericOrder = (label: string) => {
|
|
const normalized = String(label).trim();
|
|
const rangeMatch = normalized.match(/^(\d+(?:\.\d+)?)~(\d+(?:\.\d+)?)m/);
|
|
if (rangeMatch) {
|
|
return {
|
|
start: Number(rangeMatch[1]),
|
|
end: Number(rangeMatch[2]),
|
|
priority: 0,
|
|
};
|
|
}
|
|
|
|
const overMatch = normalized.match(/^(\d+(?:\.\d+)?)m\s*이상/);
|
|
if (overMatch) {
|
|
const value = Number(overMatch[1]);
|
|
return {
|
|
start: value,
|
|
end: value,
|
|
priority: 1,
|
|
};
|
|
}
|
|
|
|
return {
|
|
start: Number.MAX_SAFE_INTEGER,
|
|
end: Number.MAX_SAFE_INTEGER,
|
|
priority: 2,
|
|
};
|
|
};
|
|
|
|
const getLegendColor = (layerInfo: any) =>
|
|
String(
|
|
layerInfo?.polgnColor ??
|
|
layerInfo?.spacColor ??
|
|
layerInfo?.lnColor ??
|
|
layerInfo?.lineColor ??
|
|
"",
|
|
).trim();
|
|
|
|
const getLegendBorderColor = (layerInfo: any) =>
|
|
String(
|
|
layerInfo?.lnColor ??
|
|
layerInfo?.lineColor ??
|
|
layerInfo?.polgnColor ??
|
|
layerInfo?.spacColor ??
|
|
"",
|
|
).trim();
|
|
|
|
const hasLegendStyle = (layerInfo: any) =>
|
|
getLegendColor(layerInfo) !== "" || getLegendBorderColor(layerInfo) !== "";
|
|
|
|
const normalizeLegendLabel = (label: string) => {
|
|
const normalized = String(label).trim();
|
|
return /(?:미만|이상)$/.test(normalized)
|
|
? normalized
|
|
: `${normalized} 미만`;
|
|
};
|
|
|
|
export const extractWaterDepthLegendItems = (
|
|
checkedNodesData: any[],
|
|
): WaterDepthLegendItem[] => {
|
|
if (!Array.isArray(checkedNodesData) || checkedNodesData.length === 0)
|
|
return [];
|
|
|
|
const items = new Map<string, WaterDepthLegendItem>();
|
|
|
|
checkedNodesData.forEach((node) => {
|
|
const layerInfo = node?.data ?? {};
|
|
const layerTypeName = getLayerTypeName(layerInfo);
|
|
if (!WATER_DEPTH_LAYER_TYPES.includes(layerTypeName)) return;
|
|
if (!hasLegendStyle(layerInfo)) return;
|
|
|
|
const itemKey = String(node?.text ?? layerInfo?.lyrNm ?? "").trim();
|
|
const itemColor = getLegendColor(layerInfo);
|
|
const itemBorderColor = getLegendBorderColor(layerInfo);
|
|
|
|
if (!itemKey || !itemColor) return;
|
|
if (!items.has(itemKey)) {
|
|
items.set(itemKey, {
|
|
key: itemKey,
|
|
label: normalizeLegendLabel(itemKey),
|
|
color: itemColor,
|
|
borderColor: itemBorderColor || itemColor,
|
|
sortSeq: getFiniteSortOrder(layerInfo?.sortSeq),
|
|
});
|
|
}
|
|
});
|
|
|
|
return Array.from(items.values()).sort((left, right) => {
|
|
const leftOrder = getLegendNumericOrder(left.key);
|
|
const rightOrder = getLegendNumericOrder(right.key);
|
|
|
|
if (leftOrder.start !== rightOrder.start)
|
|
return leftOrder.start - rightOrder.start;
|
|
if (leftOrder.priority !== rightOrder.priority)
|
|
return leftOrder.priority - rightOrder.priority;
|
|
if (leftOrder.end !== rightOrder.end) return leftOrder.end - rightOrder.end;
|
|
if (left.sortSeq !== right.sortSeq) return left.sortSeq - right.sortSeq;
|
|
return left.label.localeCompare(right.label, "ko");
|
|
});
|
|
};
|
|
|
|
interface WaterDepthProps {
|
|
onClose?: () => void;
|
|
isPlaying: boolean;
|
|
setIsPlaying: (isPlaying: boolean) => void;
|
|
totalSteps?: number;
|
|
currentStep?: number;
|
|
legendItems?: WaterDepthLegendItem[];
|
|
}
|
|
|
|
export function WaterDepth({
|
|
onClose,
|
|
isPlaying,
|
|
setIsPlaying,
|
|
totalSteps = 0,
|
|
currentStep = 0,
|
|
legendItems = [],
|
|
}: WaterDepthProps) {
|
|
useEffect(() => {
|
|
if (setIsPlaying) setIsPlaying(isPlaying);
|
|
}, [isPlaying, setIsPlaying]);
|
|
|
|
const handlePlayToggle = () => {
|
|
setIsPlaying(!isPlaying);
|
|
};
|
|
|
|
// 진행률 계산 (0-100%)
|
|
const progressPercentage =
|
|
totalSteps > 0 ? (currentStep / totalSteps) * 100 : 100;
|
|
|
|
return (
|
|
<>
|
|
<div className="w-[160px] rounded-lg border border-border bg-background px-4 py-3 shadow-lg">
|
|
<div className="mx-2 my-4 flex items-center justify-between">
|
|
<Label className="text-3xl font-bold text-foreground">
|
|
침수심 범례
|
|
</Label>
|
|
<button
|
|
type="button"
|
|
className="ml-2 mt-1"
|
|
onClick={handlePlayToggle}
|
|
>
|
|
{isPlaying ? (
|
|
<img
|
|
src="/images/map/toolbar/pause_button.png"
|
|
alt="pause"
|
|
className="h-6 w-6 object-contain"
|
|
/>
|
|
) : (
|
|
<img
|
|
src="/images/map/toolbar/play_button_arrowhead.png"
|
|
alt="play"
|
|
className="h-6 w-6 object-contain"
|
|
/>
|
|
)}
|
|
</button>
|
|
</div>
|
|
<div className="mb-3">
|
|
<div className="h-1 w-full rounded-full bg-muted">
|
|
<div
|
|
className="h-1 rounded-full bg-blue-900 transition-all duration-300 ease-in-out"
|
|
style={{ width: `${progressPercentage}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mx-auto flex w-[140px] flex-col items-center">
|
|
{/* 침수심 범례 제목은 지도 수심정보를 구분하는 의미색이므로 고정 청색을 유지한다. */}
|
|
<div className="mb-2 w-full rounded bg-blue-900 py-1 text-center text-2xl text-white">
|
|
침수심 범례
|
|
</div>
|
|
<div className="w-full space-y-1">
|
|
{legendItems.map((item) => (
|
|
<LegendItem
|
|
key={item.key}
|
|
color={item.color}
|
|
borderColor={item.borderColor}
|
|
label={item.label}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="mx-auto mt-1 w-full border-t-2 border-blue-900" />
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function LegendItem({
|
|
color,
|
|
borderColor,
|
|
label,
|
|
}: {
|
|
color: string;
|
|
borderColor: string;
|
|
label: string;
|
|
}) {
|
|
return (
|
|
<div className="flex w-full items-center gap-2">
|
|
<span
|
|
className="inline-block h-6 w-16"
|
|
style={{ backgroundColor: color, border: `1px solid ${borderColor}` }}
|
|
/>
|
|
<span className="text-xl font-bold text-foreground">{label}</span>
|
|
</div>
|
|
);
|
|
}
|