511 lines
17 KiB
TypeScript
511 lines
17 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 { useAlertStore } from "@/shared/lib";
|
|
|
|
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 alert = useAlertStore((state) => state.setMessage);
|
|
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("침입수 분석결과를 지도에 연계하지 못했습니다.");
|
|
}
|
|
},
|
|
[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, alert]);
|
|
|
|
return {
|
|
activeMapView,
|
|
clearLayers,
|
|
handleOverlayFeatureSelect,
|
|
};
|
|
};
|