Files
frontend/apps/sn/app/(map-layout)/map/components/inftwtResult/useInftwtResultMap.ts
T

509 lines
16 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from "react";
import GeoJSON from "ol/format/GeoJSON.js";
import VectorLayer from "ol/layer/Vector.js";
import VectorSource from "ol/source/Vector.js";
import Stroke from "ol/style/Stroke.js";
import Fill from "ol/style/Fill.js";
import Style from "ol/style/Style.js";
import { intersects as intersectsExtent } from "ol/extent";
import { getRawJson } from "@/components/lib/api";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import { getSigunguCenter, moveMapToCenter, sidoCenter } from "@common/business_components/ui/map/searchBar/utils";
import {
INFTWT_RESULT_OVERLAY_TYPE,
INFTWT_RESULT_ROW_SELECT_MESSAGE,
INFTWT_RESULT_ROW_SELECT_STORAGE_KEY,
type InftwtResultMapViewResponse,
type InftwtResultRow,
} from "./inftwtResultTypes";
type UseInftwtResultMapParams = {
gisMaps: MutableRefObject<any | null>;
setFeatureData: (value: any) => void;
setIsOpen: (value: boolean) => void;
setIsSearchModalOpen: (value: boolean) => void;
};
const INFTWT_ZONE_LAYER_ID = "SN_INFTWT_RESULT_ZONE_LAYER";
const INFTWT_PIPE_LAYER_ID = "SN_INFTWT_RESULT_PIPE_LAYER";
const DEFAULT_RESULT_FIT_PADDING: [number, number, number, number] = [120, 120, 120, 120];
const INFTWT_PROC_SEARCH_EVENT = "SN_INFTWT_PROC_SEARCH";
const zoneStyle = new Style({
fill: new Fill({
color: "rgba(16, 185, 129, 0.06)",
}),
stroke: new Stroke({
color: "rgba(5, 150, 105, 0.72)",
width: 2,
lineDash: [10, 8],
}),
zIndex: 19991,
});
const buildPipeStyle = (hasOilq: boolean) => {
const innerColor = hasOilq ? "rgba(236, 72, 153, 0.98)" : "rgba(14, 165, 233, 0.98)";
const haloWidth = hasOilq ? 11 : 5;
const innerWidth = hasOilq ? 6 : 2.5;
const dashPattern = hasOilq ? [18, 9] : undefined;
return [
new Style({
stroke: new Stroke({
color: "rgba(15, 23, 42, 0.58)",
width: hasOilq ? haloWidth + 4 : haloWidth + 2,
lineCap: "round",
lineJoin: "round",
}),
zIndex: hasOilq ? 20000 : 19999,
}),
new Style({
stroke: new Stroke({
color: "rgba(255, 255, 255, 0.98)",
width: haloWidth,
lineCap: "round",
lineJoin: "round",
}),
zIndex: hasOilq ? 20001 : 20000,
}),
new Style({
stroke: new Stroke({
color: innerColor,
width: innerWidth,
lineCap: "round",
lineJoin: "round",
lineDash: dashPattern,
}),
zIndex: hasOilq ? 20002 : 20001,
}),
];
};
const getFeatureCollection = (payload: any) => {
if (payload?.type === "FeatureCollection") return payload;
if (payload?.data?.type === "FeatureCollection") return payload.data;
return { type: "FeatureCollection", features: [] };
};
const normalizeProjectionName = (value: string) => {
const trimmed = String(value ?? "").trim();
if (!trimmed) return "EPSG:4326";
if (trimmed.startsWith("EPSG:")) return trimmed;
const match = trimmed.match(/EPSG[:/]{1,2}(\d{4,5})/i) || trimmed.match(/(\d{4,5})$/);
return match?.[1] ? `EPSG:${match[1]}` : trimmed;
};
const resolveFeatureCollectionProjection = (payload: any) => {
const featureCollection = getFeatureCollection(payload);
const crsName = String(featureCollection?.crs?.properties?.name ?? "").trim();
return normalizeProjectionName(crsName || GisApp.TO_PROJ);
};
const getOverlayFlag = (data: any) =>
String(
data?.get?.("analysisOverlayType") ??
data?.values_?.analysisOverlayType ??
data?.analysisOverlayType ??
"",
).trim();
const getOverlayBasePplMngNo = (data: any) =>
String(
data?.get?.("fcltMngNo") ??
data?.values_?.fcltMngNo ??
data?.values_?.fclt_mng_no ??
data?.fcltMngNo ??
data?.fclt_mng_no ??
"",
).trim();
const getOverlayAnalysisPplMngNo = (data: any) =>
String(
data?.get?.("pplMngNo") ??
data?.values_?.pplMngNo ??
data?.pplMngNo ??
"",
).trim();
const getOverlayAnalysisSn = (data: any) =>
Number(
data?.get?.("inftwtAnlsSn") ??
data?.values_?.inftwtAnlsSn ??
data?.inftwtAnlsSn ??
0,
);
const getOverlayAplcnYm = (data: any) =>
String(
data?.get?.("aplcnYm") ??
data?.values_?.aplcnYm ??
data?.aplcnYm ??
"",
).trim();
const getOverlayAnalysisInfo = (data: any) =>
String(
data?.get?.("analysisInfo") ??
data?.values_?.analysisInfo ??
data?.analysisInfo ??
"",
).trim();
const getFirstFeatureId = (payload: any) => {
const featureCollection = getFeatureCollection(payload);
const firstFeature = Array.isArray(featureCollection?.features) ? featureCollection.features[0] : null;
return String(firstFeature?.id ?? "").trim();
};
const getOilqPipeExtent = (features: any[]) => {
const oilqFeatures = features.filter((feature) => Boolean(feature?.get?.("hasOilq")));
if (oilqFeatures.length === 0) return null;
let nextExtent: [number, number, number, number] | null = null;
oilqFeatures.forEach((feature) => {
const geometryExtent = feature?.getGeometry?.()?.getExtent?.();
if (!Array.isArray(geometryExtent) || geometryExtent.some((value: number) => !Number.isFinite(value))) {
return;
}
if (!nextExtent) {
nextExtent = [...geometryExtent] as [number, number, number, number];
return;
}
nextExtent = [
Math.min(nextExtent[0], geometryExtent[0]),
Math.min(nextExtent[1], geometryExtent[1]),
Math.max(nextExtent[2], geometryExtent[2]),
Math.max(nextExtent[3], geometryExtent[3]),
];
});
return nextExtent;
};
export const useInftwtResultMap = ({
gisMaps,
setFeatureData,
setIsOpen,
setIsSearchModalOpen,
}: UseInftwtResultMapParams) => {
const activeResultRowRef = useRef<InftwtResultRow | null>(null);
const zoneLayerRef = useRef<VectorLayer<any> | null>(null);
const zoneSourceRef = useRef<VectorSource | null>(null);
const pipeLayerRef = useRef<VectorLayer<any> | null>(null);
const pipeSourceRef = useRef<VectorSource | null>(null);
const mapViewCacheRef = useRef<Map<number, InftwtResultMapViewResponse>>(new Map());
const [activeMapView, setActiveMapView] = useState<InftwtResultMapViewResponse | null>(null);
const clearLayers = useCallback(() => {
zoneSourceRef.current?.clear?.();
pipeSourceRef.current?.clear?.();
zoneLayerRef.current?.changed?.();
pipeLayerRef.current?.changed?.();
gisMaps.current?.renderSync?.();
}, [gisMaps]);
const ensureZoneLayer = useCallback(() => {
const map = gisMaps.current;
if (!map) return null;
if (zoneLayerRef.current && zoneSourceRef.current) {
return zoneSourceRef.current;
}
const source = new VectorSource();
const layer = new VectorLayer({
source,
zIndex: 19991,
});
layer.set("id", INFTWT_ZONE_LAYER_ID);
layer.set("lyrNm", "移⑥엯?섎텇??泥섎━援ъ뿭");
layer.set("lyrTyNm", "tb_ms_swage_prcs_zone");
layer.set("clickPsblYn", "N");
layer.setVisible?.(true);
map.addLayer(layer);
zoneLayerRef.current = layer;
zoneSourceRef.current = source;
return source;
}, [gisMaps]);
const ensurePipeLayer = useCallback(() => {
const map = gisMaps.current;
if (!map) return null;
if (pipeLayerRef.current && pipeSourceRef.current) {
return pipeSourceRef.current;
}
const source = new VectorSource();
const layer = new VectorLayer({
source,
zIndex: 19997,
});
layer.set("id", INFTWT_PIPE_LAYER_ID);
layer.set("lyrNm", "침입수분석 결과관로");
layer.set("lyrTyNm", "tb_ms_swage_ppl");
layer.set("clickPsblYn", "Y");
layer.setVisible?.(true);
map.addLayer(layer);
pipeLayerRef.current = layer;
pipeSourceRef.current = source;
return source;
}, [gisMaps]);
const fitToRenderedFeatures = useCallback(
(mapView?: InftwtResultMapViewResponse | null) => {
const map = gisMaps.current;
const zoneExtent = zoneSourceRef.current?.getExtent?.();
const pipeExtent = pipeSourceRef.current?.getExtent?.();
const pipeFeatures = pipeSourceRef.current?.getFeatures?.() ?? [];
const oilqPipeExtent = getOilqPipeExtent(pipeFeatures);
const hasPipeFeatures = pipeFeatures.length > 0;
const hasZoneFeatures = Boolean(zoneSourceRef.current?.getFeatures?.().length);
const preferZone = mapView?.focusTargetType === "ZONE" && hasZoneFeatures;
const zoneExtentIsFinite = Array.isArray(zoneExtent) && zoneExtent.every((value: number) => Number.isFinite(value));
const pipeExtentIsFinite = Array.isArray(pipeExtent) && pipeExtent.every((value: number) => Number.isFinite(value));
const hasPipeInsideZone =
preferZone && zoneExtentIsFinite
? pipeFeatures.some((feature: any) => {
const geometryExtent = feature?.getGeometry?.()?.getExtent?.();
return Array.isArray(geometryExtent)
&& geometryExtent.every((value: number) => Number.isFinite(value))
&& intersectsExtent(zoneExtent as [number, number, number, number], geometryExtent as [number, number, number, number]);
})
: false;
const shouldUseZoneExtent = preferZone && (hasPipeInsideZone || !hasPipeFeatures);
const extent = oilqPipeExtent
? oilqPipeExtent
: shouldUseZoneExtent
? zoneExtent
: hasPipeFeatures
? pipeExtent
: hasZoneFeatures
? zoneExtent
: null;
if (!map || !extent || extent.some((value: number) => !Number.isFinite(value))) {
return;
}
map.getView?.().fit?.(extent, {
duration: 350,
padding: DEFAULT_RESULT_FIT_PADDING,
maxZoom: oilqPipeExtent ? 17 : shouldUseZoneExtent ? 15 : 16,
constrainResolution: true,
nearest: true,
});
},
[gisMaps],
);
const moveByRegionCodes = useCallback(
(ctpvCd?: string, sggCd?: string) => {
const normalizedSggCd = String(sggCd ?? "").trim();
if (normalizedSggCd && normalizedSggCd !== "0") {
const sigunguCenter = getSigunguCenter(normalizedSggCd);
if (sigunguCenter) {
moveMapToCenter(gisMaps, sigunguCenter, 12);
return true;
}
}
const normalizedCtpvCd = String(ctpvCd ?? "").trim();
if (normalizedCtpvCd && normalizedCtpvCd !== "0") {
const sidoCenterCoordinate = sidoCenter[normalizedCtpvCd.slice(0, 2)];
if (sidoCenterCoordinate) {
moveMapToCenter(gisMaps, sidoCenterCoordinate, 11);
return true;
}
}
return false;
},
[gisMaps],
);
const moveToStableFocus = useCallback(
async (mapView: InftwtResultMapViewResponse) => {
if ((mapView.oilqPipeCount ?? 0) > 0) {
fitToRenderedFeatures(mapView);
return;
}
const activeRow = activeResultRowRef.current;
const movedByRegion = moveByRegionCodes(
activeRow?.ctpvCd ?? mapView.analysis?.ctpvCd,
activeRow?.sggCd ?? mapView.analysis?.sggCd,
);
if (movedByRegion) return;
fitToRenderedFeatures(mapView);
},
[fitToRenderedFeatures, moveByRegionCodes],
);
const renderMapView = useCallback(
async (mapView: InftwtResultMapViewResponse) => {
const zoneSource = ensureZoneLayer();
const pipeSource = ensurePipeLayer();
if (!zoneSource || !pipeSource) return;
clearLayers();
const zoneDataProjection = resolveFeatureCollectionProjection(mapView.zoneGeometry);
const zoneFeatures = new GeoJSON().readFeatures(getFeatureCollection(mapView.zoneGeometry), {
dataProjection: zoneDataProjection,
featureProjection: GisApp.TO_PROJ,
});
zoneFeatures.forEach((feature: any) => feature.setStyle(zoneStyle));
zoneSource.addFeatures(zoneFeatures);
zoneSource.changed?.();
zoneLayerRef.current?.setVisible?.(true);
zoneLayerRef.current?.changed?.();
const pipeDataProjection = resolveFeatureCollectionProjection(mapView.pipeGeometry);
const pipeFeatures = new GeoJSON().readFeatures(getFeatureCollection(mapView.pipeGeometry), {
dataProjection: pipeDataProjection,
featureProjection: GisApp.TO_PROJ,
});
pipeFeatures.forEach((feature: any) => {
const hasOilq = Boolean(feature.get?.("hasOilq"));
feature.setStyle(buildPipeStyle(hasOilq));
});
pipeSource.addFeatures(pipeFeatures);
pipeSource.changed?.();
pipeLayerRef.current?.setVisible?.(true);
pipeLayerRef.current?.changed?.();
gisMaps.current?.renderSync?.();
console.info("移⑥엯??FE overlay ?뚮뜑 寃곌낵", {
focusTargetType: mapView.focusTargetType,
zoneFeatureCount: zoneFeatures.length,
pipeFeatureCount: pipeFeatures.length,
});
await moveToStableFocus(mapView);
},
[clearLayers, ensurePipeLayer, ensureZoneLayer, gisMaps, moveToStableFocus],
);
const handleRowSelect = useCallback(
async (row: InftwtResultRow) => {
try {
activeResultRowRef.current = row;
setFeatureData(null);
setIsOpen(false);
setIsSearchModalOpen(false);
const cachedMapView = mapViewCacheRef.current.get(row.inftwtAnlsSn);
let mapView = cachedMapView;
if (!mapView) {
const payload = await getRawJson(`/sewerage-service/api/v1/inftwtAnlsRslt/mapView/${row.inftwtAnlsSn}`);
mapView = (payload?.data ?? payload) as InftwtResultMapViewResponse;
mapViewCacheRef.current.set(row.inftwtAnlsSn, mapView);
}
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent(INFTWT_PROC_SEARCH_EVENT, {
detail: {
ctpvCd: row.ctpvCd ?? "0",
sggCd: row.sggCd ?? "0",
swtrmtFcltNm: row.swtrmtFcltNm ?? "",
suppressMapMove: (mapView.oilqPipeCount ?? 0) > 0,
},
}),
);
}
setActiveMapView(mapView);
void renderMapView(mapView);
} catch (error) {
console.error("移⑥엯??遺꾩꽍寃곌낵 吏€???곌퀎 以??ㅻ쪟:", error);
alert("移⑥엯??遺꾩꽍寃곌낵瑜?吏€?꾩뿉 ?곌퀎?섏? 紐삵뻽?듬땲??");
}
},
[renderMapView, setFeatureData, setIsOpen, setIsSearchModalOpen],
);
useEffect(() => {
const handlePopupMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (event.data?.type !== INFTWT_RESULT_ROW_SELECT_MESSAGE || !event.data?.row) return;
void handleRowSelect(event.data.row as InftwtResultRow);
};
const handleStorageEvent = (event: StorageEvent) => {
if (event.key !== INFTWT_RESULT_ROW_SELECT_STORAGE_KEY || !event.newValue) return;
try {
const payload = JSON.parse(event.newValue) as { row?: InftwtResultRow };
if (!payload?.row) return;
void handleRowSelect(payload.row);
} catch (error) {
console.warn("移⑥엯??遺꾩꽍寃곌낵 storage ?숆린??媛믪쓣 ?댁꽍?섏? 紐삵뻽?듬땲??", error);
}
};
window.addEventListener("message", handlePopupMessage);
window.addEventListener("storage", handleStorageEvent);
return () => {
window.removeEventListener("message", handlePopupMessage);
window.removeEventListener("storage", handleStorageEvent);
};
}, [handleRowSelect]);
useEffect(() => {
return () => {
clearLayers();
};
}, [clearLayers]);
const handleOverlayFeatureSelect = useCallback((data: any) => {
if (getOverlayFlag(data) !== INFTWT_RESULT_OVERLAY_TYPE) {
return false;
}
const overlayAnalysisSn = getOverlayAnalysisSn(data);
const basePplMngNo = getOverlayBasePplMngNo(data);
const analysisPplMngNo = getOverlayAnalysisPplMngNo(data);
const currentRow = activeResultRowRef.current;
const popupWindow = openWindowPopup("infilChart", {
pplMngNo: basePplMngNo || analysisPplMngNo.replace(/^L/, ""),
inftwtAnlsSn: overlayAnalysisSn || currentRow?.inftwtAnlsSn || 0,
aplcnYm: getOverlayAplcnYm(data) || currentRow?.aplcnYm || activeMapView?.analysis?.aplcnYm || "",
analysisInfo:
getOverlayAnalysisInfo(data) ||
currentRow?.anlsInfo ||
activeMapView?.analysis?.anlsInfo ||
"",
resultOnly: "Y",
});
if (!popupWindow) {
alert("?앹뾽??李⑤떒?섏뿀?듬땲?? 釉뚮씪?곗? ?앹뾽 ?덉슜 ?ㅼ젙???뺤씤?댁<?몄슂.");
}
return true;
}, [activeMapView]);
return {
activeMapView,
clearLayers,
handleOverlayFeatureSelect,
};
};