Merge branch 'iipbs/sn/dev' of https://www.onasys.co.kr:3000/ONAANDALL/frontend into iipbs/wr/sttn

This commit is contained in:
unknown
2025-05-13 09:36:57 +09:00
236 changed files with 9368 additions and 2 deletions
@@ -31,6 +31,7 @@
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2",
"axios": "1.8.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
@@ -0,0 +1,172 @@
import './lib/css/gis.common.css';
import React, { createContext, useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@common/components/ui/tabs';
import OlMap from 'ol/Map';
import View from 'ol/View';
import { defaults as defaultControls, OverviewMap } from 'ol/control';
import { defaults } from 'ol/interaction';
import * as olProj from 'ol/proj';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import GisApp from './lib/gis.core';
import GisContent from './GisContent';
import GisLonLat from './GisLonLat';
import GisToolBar from './GisToolBar';
import GisTab from './GisTab';
import MapToggleBtn from './MapToggleBtn';
import FeatureClickEventPopup from '@/map/featureClickEventPopup';
export const GisContext = createContext({gisCore: null, gisMap: null, treeData:null, checkedLayerIds:null});
export const useGisContext = () => useContext(GisContext);
function Gis({ treeData = [], isSearch = false }) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [featureData,setFeatureData] = useState(null) // 선택된 피쳐 데이터
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref
const checkedLayerIds = useRef([]);
let gisMap = null;
const closePopup = () => {
setIsOpen(false); // 팝업 닫기
setFeatureData(null)
};
const handleFeatureSelect = (data) => {
console.log('check',data)
setFeatureData(data); // 선택된 피처 데이터 저장
setIsOpen(true); // 팝업 열기
};
// SearchModal이 열릴 때 featureClickEventPopup 닫기
useEffect(() => {
if (isSearchModalOpen && isOpen) {
setIsOpen(false);
setFeatureData(null);
}
}, [isSearchModalOpen]);
useEffect(() => {
if (!mapRef.current) return;
if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단
// EPSG:5179 등록
proj4.defs([
[
'EPSG:5179',
'+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',
],
]);
register(proj4);
//맵 생성
const map = new OlMap({
layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']],
renderer: 'webgl',
target: mapRef.current,
interactions: defaults({ doubleClickZoom: false }),
controls: defaultControls({
zoom: false,
rotate: false,
attributionOptions: {
collapsible: false,
},
}).extend([]),
view: new View({
projection: GisApp.TO_PROJ,
zoom: gisCore.defaults.zoom,
center: olProj.transform(
[126.85306, 35.571634],
GisApp.FROM_PROJ,
GisApp.TO_PROJ
),
minZoom: gisCore.defaults.minZoom,
maxZoom: gisCore.defaults.maxZoom,
}),
});
//미니맵
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
view: new View({
projection: GisApp.TO_PROJ,
minZoom: 9,
}),
collapsed: false,
collapseLabel: '\u00BB',
label: '\u00AB',
});
map.addControl(overviewMap);
gisCore.__proto__.map = map;
gisCore.scaleControl();
gisCore.changeBaseLayer('base');
setMapObject({ map: map });
mapInstanceRef.current = map; // 생성 완료 플래그 설정
}, []);
useEffect(() => {
if (!treeData || treeData.length === 0) return;
gisCore.deleteAllOverlay();
gisCore.deleteAllLayer();
const layerCode = gisCore.convertLayerInfosToLayerCode(treeData, 'lyrSn');
GisApp.layerCode = layerCode;
//setTreeData(data);
const layerItems = getLayerItems(layerCode);
//setEditLayerItems(prev => [...layerItems])
}, [treeData]);
const getLayerItems= (layerCode) => {
const layerItems = Object.entries(layerCode)
.filter(([key, obj], idx) => obj?.updtPsblYn === 'Y')
.map(([key, obj]) => ({
...obj,
name: obj.lyrNm,
label: obj.lyrNm,
value: obj.lyrSn + '',
}))
.sort((a, b) => {
if (a.sortOrdr > b.sortOrdr) return 1;
if (a.sortOrdr < b.sortOrdr) return -1;
return 0;
});
return layerItems;
};
if (mapObject.map) {
try {
gisMap = mapObject.map;
} catch (e) {
console.error('오류 발생:', e); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
}
return (
<>
<GisContext.Provider value={{gisCore, gisMap, treeData, checkedLayerIds}}>
<div className="h-[calc(100vh-10rem)] flex relative">
<GisTab visible={tabVisible} setVisible={setTabVisible} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} />
<GisContent onFeatureSelect={handleFeatureSelect}/>
{isOpen && featureData && (
<FeatureClickEventPopup
featureData={featureData}
onClose={closePopup} // 닫기 콜백 전달
/>
)}
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
{/* <GisLonLat/>
<GisToolBar/>
<MapToggleBtn/> */}
</div>
</GisContext.Provider>
</>
);
}
export default React.memo(Gis);
@@ -0,0 +1,60 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import GisApp from './lib/gis.core';
import { useGisContext } from './Gis';
import GisLonLat from './GisLonLat';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import './lib/css/gis.edit.css';
import MapToggleBtn from './MapToggleBtn';
import GisEdit from './GisEdit';
export default function GisContent({ edit = false, onFeatureSelect }) {
const { gisMap, gisCore } = useGisContext();
const [visibleComponents, setVisibleComponents] = useState({});
const addressResultLayer = useRef(null);
const [playShow, setPlayShow] = useState({ show: false });
//셀렉트 버튼 이벤트
// const clickFeature = useCallback(feature => {
// if (feature) {
// const layer = feature.getLayer();
// setVisibleComponents(prevState => ({
// ...prevState,
// }));
// }
// }, []);
const selectRslt = useCallback(feature => {
GisApp.Module.layer.highLightFeature(
[feature],
{
id: GisApp.hlLayerId,
zIndex: 10,
opacity: 1,
selectable: true,
},
{ width: 30 }
);
clickFeature(feature);
}, []);
useEffect(() => {
if (gisMap) {
setVisibleComponents(prevState => ({
...prevState,
}));
}
}, [gisMap]);
if (!gisMap) return null;
return (
<>
{/*<GisTree />*/}
<MapToggleBtn />
<GisLonLat />
<GisToolBar />
<GisSelect onFeatureSelect={onFeatureSelect}/>
</>
);
}
@@ -0,0 +1,173 @@
import GisApp from './lib/gis.core';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { GisContext, setTree } from './Gis';
import { Button } from "@common/components";
import { gAlert, gConfirm } from './lib/util';
// import { fclApi } from '../../js/apis/fcl/fclApi';
export default React.memo(function GisEdit() {
const { editLayerItems = [], siteSn, locgovCd, checkedLayerIds } = useContext(GisContext);
const editModule = GisApp.Module.edit;
const ftrIdn = useRef(null);
const selectedLyrId = useRef(null);
if (editLayerItems.length > 0 && !selectedLyrId.current) {
selectedLyrId.current = editLayerItems[0].value;
}
const [btnEvt, setBtnEvt] = useState({ edit: false, modify: false, add: false, snap: false });
const edit = () => {
const edit = !btnEvt.edit;
if (edit) {
setBtnEvt({ ...btnEvt, edit: edit });
} else {
setBtnEvt({ edit: edit, modify: false, add: false, snap: false });
editModule.clearUndoRedoFeature();
}
editModule.removeInteraction();
};
const modify = () => {
const modify = !btnEvt.modify;
setBtnEvt({ ...btnEvt, ...{ add: false }, modify });
if (modify) {
editModule.setModify();
} else {
editModule.removeModify();
}
};
const add = () => {
let add = !btnEvt.add;
if (add) {
if (!checkedLayerIds?.current?.includes(selectedLyrId.current)) {
alert('트리에서 레이어 체크 후 추가 가능합니다.');
return;
}
const errMsg = editModule.setDraw(selectedLyrId.current);
if (errMsg) {
alert(errMsg);
return;
}
} else {
editModule.removeDraw();
}
setBtnEvt({ ...btnEvt, ...{ modify: false }, add });
};
const del = () => {
editModule.deleteFeature();
};
const undo = () => {
GisApp.Module.edit.undo();
};
const redo = () => {
GisApp.Module.edit.redo();
};
const snap = () => {
const snap = !btnEvt.snap;
setBtnEvt({ ...btnEvt, snap });
if (snap) {
editModule.setSnap();
} else {
editModule.removeSnap();
}
};
const validateEditFeatures = editFeatures => {
if (editFeatures.length === 0) {
//gAlert('INFO', 'NO_CHANGED_DATA');
return false;
}
return true;
};
const save = async () => {
const editFeatureInfo = editModule.getEditFeatureInfo();
const editFeatures = Object.values(editFeatureInfo).map(value => ({
siteSn,
locgovCd,
geom: value.geom,
flag: value.flag,
lyrId: value.lyr_id,
}));
if (!validateEditFeatures(editFeatures)) {
return;
}
if (!gConfirm('CONFIRM', 'SAVE_QUEST')) return false;
// const result = await fclApi.saveEditFeatures(editFeatures);
// if (result.code === 200) {
// editModule.clearUndoRedo();
// }
};
useEffect(() => {
//setBtnEvt({ edit: false, modify: false, add: false, snap: false });
editModule.setUndoInteraction();
editModule.removeInteraction();
}, [locgovCd]);
if (editLayerItems.length === 0) return;
return (
<>
<div className="edit_control map_control_div ol-unselectable ol-control" style={{ pointerEvents: 'auto', top: '28px', left: '120px', zIndex: '1' }}>
<label htmlFor={'selectLyrId'}></label>
<select
id={'selectLyrId'}
onChange={evt => {
const lyrId = evt.target.value;
selectedLyrId.current = lyrId + '';
setBtnEvt({ ...btnEvt, ...{ add: false } });
editModule.setInteractionActive(false);
}}
>
{editLayerItems.map(layerItem => (
<option value={layerItem?.lyrSn} key={layerItem?.lyrSn}>
{layerItem?.lyrNm}
</option>
))}
</select>
<Button btn={{ type: `${btnEvt.edit ? 'primary' : 'secondary'}`, fnc: edit }}>{`${btnEvt.edit ? ' 편집종료' : '편집하기'}`}</Button>
{btnEvt.edit && (
<ul>
<li>
<div className={`btn_edit_slt${btnEvt.modify ? ' active' : ''}`} title="수정" onClick={modify}>
<span>수정</span>
</div>
</li>
<li>
<div className={`btn_edit_add${btnEvt.add ? ' active' : ''}`} title="추가" onClick={add}>
<span>추가</span>
</div>
</li>
<li>
<div className="btn_edit_del" title="삭제" onClick={del}>
<span>삭제</span>
</div>
</li>
<li>
<div className="btn_edit_prev" title="이전" onClick={undo}>
<span>이전</span>
</div>
</li>
<li>
<div className="btn_edit_next" title="다음" onClick={redo}>
<span>다음</span>
</div>
</li>
<li>
<div className={`btn_edit_snap${btnEvt.snap ? ' active' : ''}`} title="스냅" onClick={snap}>
<span>스냅</span>
</div>
</li>
<li>
<div className="btn_edit_save" title="저장" onClick={save}>
<span>저장</span>
</div>
</li>
</ul>
)}
</div>
</>
);
});
@@ -0,0 +1,44 @@
import React from 'react';
import GisApp from './lib/gis.core';
import { MousePosition } from 'ol/control';
import { useContext, useEffect, useRef } from 'react';
import { GisContext } from './Gis';
export default React.memo(function GisLonLat() {
const { gisCore } = useContext(GisContext);
const labelLonLatRef = useRef(null);
useEffect(() => {
if (!gisCore?.map || !labelLonLatRef.current) return;
// 기존 MousePosition 제거를 위한 컨트롤 추적
let existingMouseControls = gisCore.map
.getControls()
.getArray()
.filter(control => control instanceof MousePosition);
// 기존 MousePosition 컨트롤 제거
existingMouseControls.forEach(control => {
gisCore.map.removeControl(control);
});
// 새로운 MousePosition 컨트롤 추가
const mousePosition = new MousePosition({
coordinateFormat: function (coordinate) {
let lng = coordinate[0];
let lat = coordinate[1];
return `경도 : ${lng.toFixed(3)} &nbsp; 위도 : ${lat.toFixed(3)}`;
},
projection: GisApp.FROM_PROJ,
className: 'custom-mouse-position',
target: labelLonLatRef.current,
});
gisCore.map.addControl(mousePosition);
}, [gisCore.map]);
return (
<>
<ul className="map-scale-area">
<li id="label_lonlat" ref={labelLonLatRef} />
</ul>
</>
);
});
@@ -0,0 +1,9 @@
import React from 'react';
export default function GisSearch() {
return (
<div className="p-2">
<p>범례 영역입니다 (향후 컴포넌트로 교체 가능)</p>
</div>
);
}
@@ -0,0 +1,55 @@
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import GisApp from './lib/gis.core';
import GisOlSelect from './lib/gis.select';
import { GisContext } from './Gis';
export default React.memo(function GisSelect({ onFeatureSelect }) {
const { gisMap } = useContext(GisContext);
// useEffect(() => {
// if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함
// const select = new GisOlSelect(gisMap);
// GisApp.Module.select = select;
// select.getSelectedInfo(function (features) {
// if (features?.[0]) {
// // onFeatureClick?.(features[0]); // props로 넘긴 함수 실행
// onFeatureSelect(layer.getProperties()); //피쳐 속성데이터 상위컴포넌트로 전달
// }
// });
// }, [gisMap, onFeatureSelect]);
const select = new GisOlSelect(gisMap);
GisApp.Module.select = select;
select.getSelectedInfo(function (features) {
//clickFeature(features[0]);
if(features.length > 0) {
const layer = features[0];
onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달
}
});
return (
<>
{/* <div className="select_control map_control_div ol-unselectable ol-control" style={{ pointerEvents: 'auto', zIndex: '1' }}>
<h3>선,면,다각형을 이용한 시설물 선택 영역</h3>
<ul>
<li>
<div className="sprite_gis icon1 btn_select_one" title="단일선택" onClick={reset}>
<span>단일선택</span>
</div>
</li>
<li>
<div className="sprite_gis icon4 btn_select_cancel" title="새로고침" onClick={reset}>
<span>새로고침</span>
</div>
</li>
</ul>
</div> */}
</>
);
});
@@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { Tabs, TabsList, TabsTrigger } from '@common/components/ui/tabs';
import GisTree from './GisTree';
import GisSearch from './GisSearch';
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
import SearchModal from '@/map/SearchModal';
function GisTab({ visible, setVisible, isSearch, isSearchModalOpen, setIsSearchModalOpen }) {
const [activeTab, setActiveTab] = useState('layers');
return (
<>
<div
className={`absolute top-0 left-0 w-[250px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-40
${visible ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`}
>
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
<TabsList className="flex-none mt-4 border-none rounded-none">
<TabsTrigger value="layers" className="flex-1 h-10">레이어</TabsTrigger>
{isSearch && (
<button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="h-10 w-10 flex items-center justify-center px-0"
>
<Search size={18} className="text-gray-700" />
</button>
// <TabsTrigger value="search" className="flex-1 h-10 flex items-center justify-center">
// <Search size={18} />
// </TabsTrigger>
)}
</TabsList>
{/* GisTree는 항상 마운트되며, 탭 상태에 따라 보여주기만 함 */}
<div
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${activeTab === 'layers' ? 'block' : 'hidden'
}`}
>
<GisTree />
</div>
{/* GisSearch도 동일하게 처리 */}
{/* {isSearch && (
<div
className={`grow overflow-y-auto p-2 pt-0 border-none shadow-none ${
activeTab === 'search' ? 'block' : 'hidden'
}`}
>
<GisSearch />
</div>
)} */}
{/* 🔍 검색 모달 */}
<SearchModal open={isSearchModalOpen} onOpenChange={setIsSearchModalOpen} />
</Tabs>
{/* 접기 버튼 */}
<button
onClick={() => setVisible(false)}
className="absolute top-1/2 right-[-3px] transform -translate-y-1/2 z-50 bg-white border border-l border-gray-300 rounded-l-none rounded-r px-1 py-2 shadow-sm"
>
<ChevronLeft size={16} />
</button>
</div>
{/* 열기 버튼 */}
{!visible && (
<button
onClick={() => setVisible(true)}
className="absolute top-1/2 left-[-3px] transform -translate-y-1/2 z-50 bg-white border border-r border-gray-300 rounded-r px-1 py-2 shadow-sm"
>
<ChevronRight size={16} />
</button>
)}
</>
);
}
export default React.memo(GisTab);
@@ -0,0 +1,71 @@
import React from 'react';
import GisApp from './lib/gis.core';
import GisOlMeasure from './lib/gis.measure';
import { useContext } from 'react';
import { useGisContext } from './Gis';
export default React.memo(function GisToolBar() {
const { gisMap } = useGisContext();
if (!gisMap) return <></>;
const gisMeasuer = new GisOlMeasure(gisMap);
GisApp.Module.measure = gisMeasuer;
const zoomIn = () => {
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
gisMap.getView().animate({
zoom: gisMap.getView().getZoom() + 1,
duration: 250,
});
}
};
const zoomOut = () => {
if (gisMap.getView().getMaxZoom() > gisMap.getView().getZoom()) {
gisMap.getView().animate({
zoom: gisMap.getView().getZoom() - 1,
duration: 250,
});
}
};
const measure = type => {
gisMeasuer.addInteraction(type);
};
const reset = () => {
gisMeasuer.removeFeature();
};
return (
<>
<div className="custom-toolbar">
<div className="tool-bar">
<h3>toolbar 영역</h3>
<ul>
<li>
<div className="btn_control_zoomin" title="확대" onClick={zoomIn}>
<span>확대</span>
</div>
</li>
<li>
<div className="btn_control_zoomout" title="축소" onClick={zoomOut}>
<span>축소</span>
</div>
</li>
<li>
<div className="btn_control_ruler btn_control_measure" title="거리 측정" onClick={() => measure(GisApp.measure.LineString)}>
<span>거리측정</span>
</div>
</li>
<li>
<div className="btn_control_area btn_control_measure" title="면적 측정" onClick={() => measure(GisApp.measure.Polygon)}>
<span>면적측정</span>
</div>
</li>
<li>
<div className="sprite_gis icon4 btn_select_cancel" title="리셋" onClick={() => reset()}>
<span>리셋</span>
</div>
</li>
</ul>
</div>
</div>
</>
);
});
@@ -0,0 +1,209 @@
import GisApp from './lib/gis.core';
import { util } from './lib/util';
import React, { useEffect, useState } from 'react';
import { useGisContext } from './Gis';
import { DndProvider } from "react-dnd";
import {
Tree,
MultiBackend,
getBackendOptions
} from "@minoru/react-dnd-treeview";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components/ui/tabs";
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => {} }) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
const [nodes, setNodes] = useState([]);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
useEffect(() => {
if (treeData && treeData.length > 0) {
const nodeData = setNodeValues(treeData);
setNodes(nodeData);
gisCore.setAllLayersOnMap(false);
}
}, [treeData]);
useEffect(() => {
if (nodes.length > 0) {
const defaultChecked = getDefaultCheckedNodes(nodes);
setChecked(defaultChecked);
const defaultExpanded = Array.from(new Set([
...defaultChecked,
...defaultChecked.flatMap((id) => getAncestors(id, nodes))
]));
setExpanded(defaultExpanded);
gisCore.setLayerOnMap(defaultChecked, true);
}
}, [nodes]);
const getDefaultCheckedNodes = (nodes, values = []) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.data.initEprssYn === 'Y') {
values.push(node.data.lyrSn);
}
}
return values;
};
const getDescendants = (id, all) => {
const children = all.filter((n) => n.parent === id);
return children.reduce(
(acc, child) => acc.concat(child.id, ...getDescendants(child.id, all)),
[]
);
};
const getAncestors = (id, all) => {
const node = all.find((n) => n.id === id);
if (!node || node.parent == null) return [];
return [node.parent, ...getAncestors(node.parent, all)];
};
const isEveryChildChecked = (parentId, all, checked) => {
const children = all.filter((n) => n.parent === parentId);
return children.every((c) => checked.includes(c.id));
};
const handleCheck = (id) => {
setChecked((prevChecked) => {
const isChecked = prevChecked.includes(id);
let newChecked = [...prevChecked];
if (isChecked) {
const descendants = getDescendants(id, nodes);
newChecked = newChecked.filter((cid) => cid !== id && !descendants.includes(cid));
const ancestors = getAncestors(id, nodes);
ancestors.forEach((p) => {
if (newChecked.includes(p)) {
newChecked = newChecked.filter((cid) => cid !== p);
}
});
} else {
const descendants = getDescendants(id, nodes);
newChecked = Array.from(new Set([...newChecked, id, ...descendants]));
const ancestors = getAncestors(id, nodes);
ancestors.forEach((p) => {
if (isEveryChildChecked(p, nodes, newChecked)) {
newChecked.push(p);
}
});
// setExpanded((prev) => Array.from(new Set([...prev, ...ancestors])));
}
const allIds = nodes.map((n) => n.id);
const unchecked = allIds.filter((nid) => !newChecked.includes(nid));
gisCore.setLayerOnMap(newChecked, true);
gisCore.setLayerOnMap(unchecked, false);
return newChecked;
});
};
const setNodeValues = (result) => {
return result.map((item) => {
const src = makeLegendSrc(item);
let label = item.lyrNm;
return {
id: item.lyrSn,
text: label,
parent: item.upperLyrSn,
droppable: !!item.children,
data: item,
icon: src ? <img src={src} alt={item.lyrNm} /> : '',
};
});
};
const makeLegendSrc = layerinfo => {
if(!layerinfo.gobjTy) return null;
let src;
if (layerinfo.iconNm) {
src = GisApp.CONTEXT_LAYER_PATH + layerinfo.iconNm;
} else {
const ss = makeLegendImage(layerinfo);
src = ss.src;
}
return src;
};
const makeLegendImage = layerinfo => {
let image = document.createElement('img');
let canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
let ctx = canvas.getContext('2d');
ctx.beginPath();
if (layerinfo.gobjTy === 'lineLabelPoligon') {
ctx.moveTo(1, 1);
ctx.lineTo(15, 1);
ctx.lineTo(15, 15);
ctx.lineTo(1, 15);
ctx.lineTo(1, 1);
} else if (layerinfo.gobjTy === 'line') {
ctx.moveTo(1, 15);
ctx.lineTo(5, 1);
ctx.lineTo(9, 15);
ctx.lineTo(15, 1);
} else {
ctx.moveTo(1, 1);
ctx.lineTo(15, 1);
ctx.lineTo(15, 15);
ctx.lineTo(1, 15);
ctx.lineTo(1, 1);
}
if (layerinfo.gobjTy === 'lineLabelPoligon') {
ctx.fillStyle = layerinfo.polgnColor;
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = layerinfo.lnColor;
ctx.stroke();
}
if (layerinfo.gobjTy === 'line') {
ctx.lineWidth = 1;
ctx.strokeStyle = layerinfo.lnColor;
ctx.stroke();
}
image.src = canvas.toDataURL('image/png');
return image;
};
if (!treeData || treeData.length === 0) return null;
return (
<>
<DndProvider backend={MultiBackend} options={getBackendOptions()}>
<Tree
tree={nodes}
rootId={null}
initialOpen={expanded}
onOpenChange={setExpanded}
render={(node, { depth, isOpen, onToggle }) => (
<div
className="flex items-center gap-2 py-1 px-2 hover:bg-gray-100 rounded"
style={{ marginInlineStart: depth * 16 + (!node.droppable ? 16 : 0) }}
>
{node.droppable && (
<i onClick={onToggle} className={`diveicon di-arrow-forward mr-2 text-base text-label ${isOpen ? 'rotate-90' : ''}`}
title={isOpen ? '닫기' : '열기'}>
</i>
)}
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer"/>
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
</div>
)}
sort={(a, b) => {
const aOrder = parseInt(a.data?.sortOrdr ?? '0');
const bOrder = parseInt(b.data?.sortOrdr ?? '0');
return aOrder - bOrder;
}}
/>
</DndProvider>
</>
);
});
@@ -0,0 +1,60 @@
import React, { useEffect, useRef, useState } from 'react';
import OLCesium from 'olcs/OLCesium';
import olView from 'ol/View.js';
import olSourceImageWMS from 'ol/source/ImageWMS.js';
import olSourceOSM from 'ol/source/OSM.js';
import olLayerImage from 'ol/layer/Image.js';
import olLayerTile from 'ol/layer/Tile.js';
import olMap from 'ol/Map.js';
import { GLOBAL } from '../../js/consts/global';
const Google3DMap = () => {
const mapRef = useRef(null);
const [ol3d, setOl3d] = useState(null);
useEffect(() => {
if (!mapRef.current) return;
const customProjSource = new olSourceImageWMS({
attributions: '© <a href="http://www.geo.admin.ch/internet/geoportal/en/home.html">National parks / geo.admin.ch</a>',
crossOrigin: 'anonymous',
params: { LAYERS: 'ch.bafu.schutzgebiete-paerke_nationaler_bedeutung' },
projection: 'EPSG:21781',
url: 'https://wms.geo.admin.ch/',
});
const { Cesium } = window;
Cesium.Ion.defaultAccessToken = GLOBAL.CESIUM_ION_TOKEN;
const ol2d = new olMap({
layers: [new olLayerTile({ source: new olSourceOSM() }), new olLayerImage({ source: customProjSource })],
target: mapRef.current,
view: new olView({
center: [860434.6266531206, 6029479.0044273855],
zoom: 6,
}),
});
const ol3dInstance = new OLCesium({
map: ol2d,
time: () => Cesium.JulianDate.now(),
});
const scene = ol3dInstance.getCesiumScene();
Cesium.createWorldTerrainAsync().then(tp => {
scene.terrainProvider = tp;
});
ol3dInstance.setEnabled(true);
setOl3d(ol3dInstance);
return () => {
ol3dInstance.setEnabled(false);
ol2d.setTarget(null);
};
}, []);
return <div ref={mapRef} style={{ width: '100%', height: '100vh' }} />;
};
export default Google3DMap;
@@ -0,0 +1,32 @@
import React, { useContext, useState } from 'react';
import { Button } from "@common/components";
import { GisContext } from './Gis';
import GisApp from './lib/gis.core';
export default React.memo(function MapToggleBtn() {
const { gisCore } = useContext(GisContext);
const [activeMap, setActiveMap] = useState({ label: '일반', type: 'base' }); // 기본 선택값
const handleSelect = mapObj => {
setActiveMap(mapObj);
gisCore.changeBaseLayer(mapObj.type);
};
return (
<div className="mapbtn" style={{ zIndex: 101 }}>
<button className="sprite_gis viewer" title="지도선택">지도 선택</button>
<ul className="choice_map">
<li className={activeMap.type === 'base' ? 'on' : ''}>
<button className="map1" id="base" title="일반지도" onClick={() => handleSelect({ label: '일반', type: 'base' })}>
<div>일반지도</div>
</button>
</li>
<li className={activeMap.type === 'satellite' ? 'on' : ''}>
<button className="map2" id="satelite" title="위성지도" onClick={() => handleSelect({ label: '위성', type: 'satellite' })}>
<div>위성지도</div>
</button>
</li>
</ul>
</div>
);
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,192 @@
@charset 'utf-8';
.edit_control {
position: absolute;
top: 20px;
left: 610px;
}
.edit_control h3 {
overflow: hidden;
font-size: 0;
line-height: 0;
text-indent: -9999px;
}
.edit_control ul {
float: left;
overflow: hidden;
height: 40px;
margin-left: 10px;
background: #fff;
font-size: 0;
border: 1px solid #9aa7b8;
border-radius: 3px;
box-shadow: 0px 0px 5px #999;
box-sizing: border-box;
padding: 0;
cursor: pointer;
}
.edit_control ul li {
display: inline-block;
width: 40px;
height: 40px;
border-right: 1px solid #eeeeee;
}
.edit_control ul li:last-child {
border-right: 0 none;
}
/*background 삭제*/
/* .edit_control ul li a {overflow:hidden;width: 40px;height: 40px;background: url(../../images/om/ico_gis_sprite.png) no-repeat;} */
.edit_control ul li a {
overflow: hidden;
width: 40px;
height: 40px;
display: inline-block;
padding: 0;
}
.edit_control ul li a span {
display: none;
overflow: hidden;
text-indent: -9999px;
}
.edit_control select {
float: left;
min-width: 150px;
height: 40px;
line-height: 40px;
margin-left: 10px;
padding-left: 10px;
font-size: 13px;
color: #666;
border: 1px solid #fff;
border-radius: 3px;
box-shadow: 0px 0px 5px #999;
}
.edit_control input {
float: left;
min-width: 150px;
height: 40px;
line-height: 40px;
margin-left: 10px;
padding-left: 10px;
font-size: 13px;
color: #666;
border: 1px solid #fff;
border-radius: 3px;
box-shadow: 0px 0px 5px #999;
}
.edit_control button {
float: left;
min-width: 80px;
height: 40px;
margin-left: 5px;
font-size: 13px;
color: #fff;
text-align: center;
border-radius: 3px;
cursor: pointer;
}
.edit_control button:hover {
background: #1357ce;
}
.edit_toolbar {
position: absolute;
left: 280px;
top: 52px;
}
.btn_edit_slt {
background: url(../images/toolbar/btn_edit_slt.png) no-repeat center;
height: 40px;
}
.btn_edit_slt:hover {
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
height: 40px;
}
.btn_edit_slt.active {
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
height: 40px;
}
.btn_edit_add {
background: url(../images/toolbar/btn_edit_add.png) no-repeat center;
height: 40px;
}
.btn_edit_add:hover {
background: url(../images/toolbar/btn_edit_add_on.png) no-repeat center;
height: 40px;
}
.btn_edit_add.active {
background: url(../images/toolbar/btn_edit_add_on.png) no-repeat center;
height: 40px;
}
.btn_edit_del {
background: url(../images/toolbar/btn_edit_del.png) no-repeat center;
height: 40px;
}
.btn_edit_del:hover {
background: url(../images/toolbar/btn_edit_del_on.png) no-repeat center;
height: 40px;
}
.btn_edit_prev {
background: url(../images/toolbar/btn_edit_prev.png) no-repeat center;
height: 40px;
}
.btn_edit_prev:hover {
background: url(../images/toolbar/btn_edit_prev_on.png) no-repeat center;
height: 40px;
}
.btn_edit_next {
background: url(../images/toolbar/btn_edit_next.png) no-repeat center;
height: 40px;
}
.btn_edit_next:hover {
background: url(../images/toolbar/btn_edit_next_on.png) no-repeat center;
height: 40px;
}
.btn_edit_snap {
background: url(../images/toolbar/btn_edit_snap.png) no-repeat center;
height: 40px;
}
.btn_edit_snap:hover {
background: url(../images/toolbar/btn_edit_snap_on.png) no-repeat center;
height: 40px;
}
.btn_edit_snap.active {
background: url(../images/toolbar/btn_edit_snap_on.png) no-repeat center;
height: 40px;
}
.btn_edit_save {
background: url(../images/toolbar/btn_edit_save.png) no-repeat center;
height: 40px;
}
.btn_edit_save:hover {
background: url(../images/toolbar/btn_edit_save_on.png) no-repeat center;
height: 40px;
}
.btn_edit_transform {
background: url(../images/toolbar/btn_edit_slt.png) no-repeat center;
height: 40px;
}
.btn_edit_transform:hover {
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
height: 40px;
}
.btn_edit_transform.active {
background: url(../images/toolbar/btn_edit_slt_on.png) no-repeat center;
height: 40px;
}
@@ -0,0 +1,7 @@
@charset 'utf-8';
/* 로드뷰 관련 */
.roadview-container {width: 100%; height: 100%; z-index:500; display:block;position: absolute; left: 0px;top: 0px;}
.roadview-container .roadview-container-1p {width: 310px; height: 190px; bottom: 15px; left: 15px; top: auto; border-radius: 2px; overflow: hidden; z-index: 1000; position: absolute;}
.roadview-container .roadview-container-2p {width: 100%; height:100%; float:left; position:relative;}
.roadview-container .roadview {position:absolute; top:0; right:0; left:0; bottom:0; z-index:0; background:#999;}
@@ -0,0 +1,206 @@
/* http://meyerweb.com/eric/tools/css/reset/
v2.0 | 20110126
License: none (public domain)
*/
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
line-height: 1;
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: "";
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
body {
background-color: #e6e6fc;
display: flex;
flex-direction: column;
align-items: center;
padding: 50px 0px;
}
.canvas {
width: 800px;
height: 500px;
background-color: white;
border-radius: 15px;
box-shadow: 6px 6px 5px grey;
}
.controls .controls__colors {
display: flex;
}
.controls__colors .controls__color {
width: 50px;
height: 50px;
border-radius: 25px;
margin-right: 5px;
cursor: pointer;
box-shadow: 3px 3px 5px grey;
}
.jscolor {
color: transparent;
font-size: 0;
line-height: 0;
width: 50px;
height: 50px;
border-radius: 25px;
border-width: 0px;
margin-right: 5px;
cursor: pointer;
box-shadow: 3px 3px 5px grey;
}
.controls {
display: flex;
flex-direction: column;
align-items: center;
}
.controls .controls__btns {
margin-top: 20px;
margin-bottom: 30px;
}
.controls__btns button:active {
transform: scale(0.98);
}
.controls__btns button {
all: unset;
cursor: pointer;
background-color: white;
padding: 5px 0px;
width: 80px;
text-align: center;
border-radius: 10px;
box-shadow: 3px 3px 3px grey;
border: 2px solid rgba(0, 0, 0, 0.2);
color: rgba(0, 0, 0, 0.8);
text-transform: uppercase;
font-weight: 600;
font-size: 12px;
}
.controls .controls__range {
margin-top: 20px;
}
@@ -0,0 +1,35 @@
@charset 'utf-8';
.ol-tooltip {
position: relative;
background: rgba(0, 0, 0, 0.5);
border-radius: 4px;
color: white;
padding: 4px 8px;
opacity: 0.7;
white-space: nowrap;
font-size: 12px;
}
.ol-tooltip-measure {
opacity: 1;
font-weight: bold;
}
.ol-tooltip-static {
background-color: #ffcc33;
color: black;
border: 1px solid white;
}
.ol-tooltip-measure:before,
.ol-tooltip-static:before {
border-top: 6px solid rgba(0, 0, 0, 0.5);
border-right: 6px solid transparent;
border-left: 6px solid transparent;
content: "";
position: absolute;
bottom: -6px;
margin-left: -7px;
left: 50%;
}
.ol-tooltip-static:before {
border-top-color: #ffcc33;
}
@@ -0,0 +1,923 @@
import MousePosition from 'ol/control/MousePosition';
import ScaleLine from 'ol/control/ScaleLine';
import Feature from 'ol/Feature';
import WKT from 'ol/format/WKT';
import LinearRing from 'ol/geom/LinearRing';
import Draw from 'ol/interaction/Draw';
import TileLayer from 'ol/layer/Tile';
import * as olProj from 'ol/proj';
import VectorSource from 'ol/source/Vector';
import XYZ from 'ol/source/XYZ';
import GeoSJON from 'ol/format/GeoJSON';
import { Fill, Stroke, Style } from 'ol/style';
import CircleStyle from 'ol/style/Circle';
import VectorLayer from 'ol/layer/Vector';
import { Point } from 'ol/geom';
import { boundingExtent } from 'ol/extent';
import olSourceImageWMS from 'ol/source/ImageWMS';
import { GeoJSON } from 'ol/format';
import GisOlEditLayer from './gis.edit';
import GisOlLayer from './gis.layer';
const GisApp = {};
/**
* GisApp 전역변수 생성
*/
//레이어 정보
GisApp.layerInfos = [];
//기본정보
//GisApp.GEOSERVER_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/ows';
//GisApp.GEOSERVER_WMS_URL = 'http://rsone.iptime.org:10010/geoserver/hasudo/wms';
GisApp.GEOSERVER_URL = '/sn/geoserver/hasudo/ows';
GisApp.GEOSERVER_WMS_URL = '/sn/geoserver/hasudo/wms';
//맵 정보
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
GisApp.VWORLD_URL = 'https://xdworld.vworld.kr/2d/Base/service/{z}/{x}/{y}.png';
GisApp.VWORLD_SATELLITE_URL = 'https://xdworld.vworld.kr/2d/Satellite/service/{z}/{x}/{y}.jpeg';
GisApp.GOOGLE_API_KEY = 'AIzaSyCf1sgeELFalfPuqlykRKBMj6X9u_DQH6M';
GisApp.GOOGLE_ROADMAP_URL = `https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_SATELLITE_URL = `https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&key=${GisApp.GOOGLE_API_KEY}`;
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
GisApp.OPENSTREET_URL = '//tile.openstreetmap.org/{z}/{x}/{y}.png';
GisApp.BASE_MAP_TYPES = [];
GisApp.FROM_PROJ = 'EPSG:4326';
GisApp.TO_PROJ = 'EPSG:3857';
GisApp.TO_DATA_PROJ = 'EPSG:4326';
GisApp.CONTEXT_LAYER_PATH = '/sn/images/map/layericon/';
GisApp.layerCode = {};
GisApp.hlLayerId = 'LightFeatureList';
GisApp.hLayerId = 'highLightFeature';
GisApp.wtlkgAnal = 'wtlkgAnal';
GisApp.Module = {
core: null,
select: null,
measure: null,
layer: null,
style: null,
edit: null,
};
/**
* GisApp.layerInfos Set
*/
GisApp.setLayerInfos = function (layerInfos) {
GisApp.layerInfos = layerInfos;
};
/**
* GisApp core
* @requires gis.layer.js
*/
GisApp.core = function (options) {
GisApp.core.prototype.initialize(options);
};
GisApp.core.prototype = {
defaults: {
target: '',
centerX: 37.2806094,
centerY: 127.0032502,
zoom: 11,
minZoom: 5,
maxZoom: 22,
},
options: {
/*1: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.3227711, centerY: 129.1836202, dataProj: 'EPSG:5186' },
4: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 35.0140499, centerY: 126.7110757, dataProj: 'EPSG:5186' },
14: { zoom: 16, minZoom: 10, maxZoom: 22, centerX: 34.9390499, centerY: 127.6960757, dataProj: 'EPSG:5186' },*/
},
epsg_5179: null,
map: null, // ol.Map
view: {}, // ol.view
interactions: {}, // interaction 모음 객체
selectedFeature: null, // 현재 선택된 feature
baseLayers: {},
baseMiniMapLayers: {},
layers: {},
initialize: function (options) {
let option = this.options['1'];
if (option) {
Object.assign(this.defaults, option);
GisApp.TO_DATA_PROJ = option.dataProj;
}
Object.assign(this.defaults, options);
GisApp.Module.core = this;
GisApp.Module.layer = new GisOlLayer(this.defaults);
GisApp.Module.edit = new GisOlEditLayer(this.defaults);
this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base');
this.baseLayers['base'] = this.getBaseMap('base');
this.baseLayers['satellite'] = this.getBaseMap('satellite');
this.baseLayers['3d'] = this.getBaseMap('3d');
},
layerType: {
point: 'POINT',
line: 'LINE',
poligon: 'POLIGON',
},
chgMngFcltOptions: function (fcltCd) {
let option = this.options[fcltCd];
Object.assign(this.defaults, option);
this.setCenter(Number(this.defaults.centerX), Number(this.defaults.centerY));
this.setZoom(15);
},
getBaseMap: function (_id) {
let url = GisApp.VWORLD_URL;
if (_id === 'satellite') {
url = GisApp.VWORLD_SATELLITE_URL;
}
let baseMap = new TileLayer({
id: _id,
visible: true,
type: 'base',
source: new XYZ({
url: url,
crossOrigin: 'anonymous',
}),
zIndex: 0,
});
return baseMap;
},
/**
* [사용]테이블명으로 레이어 아이디 조회
*/
getLayerCodeByTbNm: function (_tableNm) {
let returnLayerCode = null;
for (let i in GisApp.layerCode) {
let layerCode = GisApp.layerCode[i];
if (layerCode.tableName === _tableNm) {
returnLayerCode = layerCode;
}
}
return returnLayerCode;
},
/**
* [사용]베이스 레이어 변경
* Parameters:
* _title - {String} 베이스 레이어 이름
*
* Returns:
*/
changeBaseLayer: function (_id) {
this.map.getLayers().forEach(function (layer) {
let type = layer.get('type');
let id = layer.get('id');
if (type == 'base') {
if (id == _id) {
layer.setVisible(true);
} else {
layer.setVisible(false);
}
}
});
},
/**
* [사용]지도 중심좌표 설정
*/
setCenter: function (x, y, transYn) {
if (!x || !y) return;
let view = this.map.getView();
if (!transYn) {
// 값이 없으면
let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
const style = new Style({
image: new CircleStyle({
radius: 5,
fill: new Fill({
color: 'rgba(255,0,0,1)',
}),
stroke: new Stroke({
color: 'rgba(255,0,0,1)',
width: 2,
}),
}),
});
// 빨간색 점 추가 (스타일 적용)
let redPoint = new Feature({
geometry: new Point(center),
});
let redPointLayer = new VectorLayer({
source: new VectorSource({
features: [redPoint],
}),
style: style,
});
redPointLayer.set('id', 'redPointLayer');
// 이전에 만들었던 빨간색 점 레이어 삭제
this.deleteLayer('redPointLayer');
// 새로운 빨간색 점 레이어를 맵에 추가
this.map.addLayer(redPointLayer);
view.setCenter(center);
} else {
view.setCenter([Number(x), Number(y)]);
}
},
/**
* [사용]주소검색 결과 지도이동
*/
setAddressResult: function (x, y, geom, addressName) {
if (!x || !y) return;
let view = this.map.getView();
let center = olProj.transform([Number(y), Number(x)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
const style = [
new Style({
image: new CircleStyle({
radius: 5,
fill: new Fill({
color: 'rgba(255,0,0,1)',
}),
stroke: new Stroke({
color: 'rgba(255,0,0,1)',
width: 2,
}),
}),
}),
new Style({
stroke: new Stroke({
color: 'blue',
width: 3,
}),
fill: new Fill({
color: 'rgba(0, 0, 255, 0.1)',
}),
}),
];
// 빨간색 점 추가 (스타일 적용)
let redPoint = new Feature({
geometry: new Point(center),
});
let redArea = new WKT().readFeature(geom, {
dataProjection: GisApp.FROM_PROJ,
featureProjection: GisApp.TO_PROJ,
});
let redPointLayer = new VectorLayer({
source: new VectorSource({
features: [redPoint, redArea],
}),
style: style,
});
redPointLayer.set('id', 'redPointLayer');
// 이전에 만들었던 빨간색 점 레이어 삭제
this.deleteLayer('redPointLayer');
// 새로운 빨간색 점 레이어를 맵에 추가
this.map.addLayer(redPointLayer);
view.animate({ center: center, zoom: 19 });
return redPointLayer;
},
/*
* [사용]zoom 설정
*/
setZoom: function (zoom) {
if (!zoom) return;
this.map.getView().setZoom(zoom);
},
/*
* [사용]zoom 정보 반환
*/
getZoom: function () {
return this.map.getView().getZoom();
},
/**
* [사용]레이어 맵에 추가(외부에서 생성된 layer를 파라미터로 받아 맵에 추가)
*/
setWfsLayerOnMap: function (layer) {
this.deleteLayer(layer.get('id'));
this.map.addLayer(layer);
return layer;
},
/**
* [사용]레이어 생성(지오서버에서 WFS 방식으로 레이어정보 가져와 레이어 생성)
*/
getWfsLayer: function (layerCodeObj) {
return GisApp.Module.layer.getWfsLayer(layerCodeObj);
},
/**
* [사용]레이어 맵에 추가(getWfsLayer 사용하여 레이어 생성)
*/
addWfsLayer: function (layerCodeObj) {
let layer = this.getWfsLayer(layerCodeObj);
this.deleteLayer(layer.get('id'));
this.map.addLayer(layer);
return layer;
},
/**
* [사용]레이어 맵에 추가(getWmsLayer 사용하여 레이어 생성)
*/
addWmsLayer: function (layerCodeObj) {
let layer = GisApp.Module.layer.getWmsLayer(layerCodeObj);
this.deleteLayer(layer.get('id'));
this.map.addLayer(layer);
return layer;
},
/**
* [사용]레이어 삭제
*/
deleteLayer: function (layerId) {
const map = this.map;
if (!layerId) return;
map.getLayers().forEach(function (layer) {
let deleteLayer = null;
if (layer != null && layer.get('id') != null) {
let vlayerId = layer.get('id');
if (vlayerId) {
if (vlayerId === layerId) {
deleteLayer = layer;
} else if (`${vlayerId}`.includes(`${layerId}`)) {
deleteLayer = layer;
}
if (deleteLayer) {
map.removeLayer(deleteLayer);
}
}
}
});
},
/**
* [사용]레이어 전체 삭제
*/
deleteAllLayer: function () {
const map = this.map;
map.getLayers().forEach(function (layer) {
if (layer?.get('type') !== 'base') {
map.removeLayer(layer);
}
});
//맵에 보이지 않는 레이어 삭제
GisApp.Module.layer?.layers.forEach(function (layer) {
map.removeLayer(layer);
});
},
/**
* [사용]레이어이름으로 레이어 조회 반환
*/
getLayerById: function (layerId) {
if (!layerId) return;
let layer = null;
this.map.getAllLayers().forEach(function (el) {
if (el != null && el?.get('id') != null) {
let vlayerId = el?.get('id');
if (Number(vlayerId) === Number(layerId)) {
layer = el;
}
}
});
if (layer != null) {
GisApp.Module.layer.layers.forEach(function (el) {
if (el != null && el?.get('id') != null) {
let vlayerId = el?.get('id');
if (Number(vlayerId) === Number(layerId)) {
layer = el;
}
}
});
}
return layer;
},
/**
* [사용]오버레이 전체 삭제
*/
deleteAllOverlay: function () {
let map = this.map;
let overlays = map.getOverlays();
if (overlays != null) {
let overlayArray = overlays.getArray();
let overlaySize = overlayArray.length;
if (overlaySize > 0) {
for (let i = overlaySize - 1; i >= 0; i--) {
let overlay = overlayArray[i];
map.removeOverlay(overlay);
}
}
}
Object.values(GisApp.Module.style.overlays).forEach(overlay => {
if (overlay) {
map.removeOverlay(overlay);
}
});
GisApp.Module.style.overlays = {};
},
/**
* [사용]오버레이 아이디로 삭제
*/
deleteOverlay: function (_id) {
let map = this.map;
let overlays = map.getOverlays();
if (overlays != null) {
let overlayArray = overlays.getArray();
let overlaySize = overlayArray.length;
if (overlaySize > 0) {
for (let i = overlaySize - 1; i >= 0; i--) {
let overlay = overlayArray[i];
let id = overlay.getId();
if (id === _id) {
map.removeOverlay(overlay);
delete GisApp.Module.style.overlays[_id];
}
}
}
}
},
/**
* [사용]맵 이벤트 삭제
*/
removeMapEventFnNm: function (handlerNm, fnNm) {
let map = this.map;
for (let i in map.listeners_[handlerNm]) {
let listener = map.listeners_[handlerNm][i];
if (listener.name == fnNm) {
map.listeners_[handlerNm].splice(i, 1);
}
}
},
/**
* [사용]위경도 -> 시분초 변환
*/
coordTrans: function (coord) {
let str = String(coord); /* String 변환 */
let split = str.split('.'); /* 소수점 split */
let hour = split[0]; /* 시간 */
let minute = parseFloat((coord - parseInt(split[0]).toFixed(6)) * 60).toFixed(6);
let min = String(minute).split('.')[0];
let second = parseFloat((minute - parseInt(min)) * 60).toFixed(2);
return hour + '도 ' + min + '분 ' + second + '초';
},
/**
* [사용]시분초 -> 위경도 변환
*/
coordTransrate: function (degree, minute, second) {
let min = (Number(minute) / 60).toFixed(2);
let sec = (Number(second) / 3600).toFixed(4);
return degree + Number(min) + Number(sec);
},
/**
* [사용]마우스 좌표 위치
*/
mousePositionControl: function () {
let element = document.getElementById('label_lonlat');
let me = this;
let mousePosition = new MousePosition({
coordinateFormat: function (coordinate) {
let text = '';
if (element) {
let lng = coordinate[0];
let lat = coordinate[1];
text += '경도 : ' + lng.toFixed(7) + ' ( ' + me.coordTrans(lng) + ' ) ';
text += '위도 : ' + lat.toFixed(7) + '( ' + me.coordTrans(lat) + ' )';
}
return text;
},
projection: GisApp.FROM_PROJ,
className: 'custom-mouse-position',
target: document.getElementById('label_lonlat'),
});
this.map.addControl(mousePosition);
},
/**
* [사용]스케일 표시
*/
scaleControl: function () {
let scale = new ScaleLine({
minWidth: 100,
units: 'metric',
});
this.map.addControl(scale);
},
/**
* [사용]레이어 맵에 셋팅
*
* Parameters:
* layerList - {Array} [layerNm : GisApp.layerCode 키]
* visible - {Boolean} 레이어 visible 여부
*
* Returns:
*/
setLayerOnMap: function (layerIds, visible, addQurryFlter) {
for (let i in layerIds) {
let id = layerIds[i];
let layer = this.getLayerById(id);
if (layer) {
layer.setVisible(visible);
} else if (visible) {
const layerCode = GisApp.layerCode[id];
const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || '');
layerCode['flterSql'] = queryFlter;
//layer = this.addWmsLayer(layerCode);
layer = this.addWfsLayer(layerCode);
}
}
if (GisApp.Module.select) {
this.setSelectInteraction();
}
if (GisApp.Module.edit.snap) {
GisApp.Module.edit.setSnap();
}
},
/**
* [사용]shp 파일 다운로드
*/
getShapeLayer: function (option) {
GisApp.Module.layer.getShapeLayer(option);
},
/**
* [사용]레이어 셀렉트 가능하게 셋팅
*/
setSelectInteraction: function () {
let layers = this.map.getLayers();
if (layers) {
for (let i in layers.array_) {
let layer = layers.array_[i];
if (layer.get('chcYn') === 'Y') {
GisApp.Module.select.addLayer(layer);
}
}
}
},
/**
* [사용]layerInfos(array) To layerCode(Object)
*/
convertLayerInfosToLayerCode: function (array, key, parentName) {
let initialValue = {};
for (let i = 0; i < array.length; i++) {
let item = {};
Object.assign(item, array[i]);
if (parentName) {
item.parentName = parentName;
}
initialValue[item[key]] = item;
if (item.children && item.children.length > 0) {
let child = this.convertLayerInfosToLayerCode(item.children, key, item.name);
Object.assign(initialValue, child);
}
if (initialValue[item[key]].children) {
delete initialValue[item[key]].children;
}
}
return initialValue;
},
/**
* [사용]피처 좌표값 WKT포맷으로 변경
*/
convertFeatureGeomToWKT: function (feature) {
let geometry = feature.getGeometry();
const wktGeom = this.convertGeomToWkt(geometry);
return wktGeom;
},
/**
* [사용]WKT포맷 Geometry로 변경
*/
convertWktToGeom: function (wtkGeom) {
let format = new WKT();
let geom = format.readGeometry(wtkGeom);
return geom;
},
/**
* [사용]좌표와 properties의 키값으로 피처 검색
*/
getFeatureGeomProp: function (geometry, ftrIds, ftrCde) {
let returnFeature;
this.map.getLayers().forEach(function (layer) {
let type = layer.get('type');
if (type != 'base') {
let source = layer.getSource();
let type = geometry.getType();
let coordinates = geometry.getCoordinates();
if (type.toUpperCase() === 'POINT') {
coordinates = [coordinates];
} else {
coordinates = coordinates[0];
}
let featureFtrIds;
let featureFtrCde;
for (let i in coordinates) {
let coordinate = coordinates[i];
let feature = source.getClosestFeatureToCoordinate(coordinate);
if (feature) {
let properties = feature.getProperties();
featureFtrIds = properties['FTR_IDS'];
featureFtrCde = properties['FTR_CDE'];
if (ftrIds == featureFtrIds && ftrCde == featureFtrCde) {
returnFeature = feature;
}
}
}
}
});
return returnFeature;
},
/**
* [사용]관리번호로 피처검색
*/
getFeatureByFtrIdn: function (typeName, ftrIdn) {
let returnFeature;
this.map.getLayers().forEach(function (layer) {
let layerTypeName = layer.get('typeName');
if (layerTypeName === typeName) {
layer
.getSource()
.getFeatures()
.forEach(feature => {
let featureFtrIdn = feature.get('ftrIdn');
if (featureFtrIdn === ftrIdn) {
returnFeature = feature;
}
});
}
});
return returnFeature;
},
/**
* [사용]맵 이벤트 추가
*/
addMapEvent: function (evtNm, id, callback) {
if (id) {
let evt = this.map.on(evtNm, callback);
evt.listener.id = id;
}
},
/**
* [사용]맵 이벤트 삭제
*/
removeMapEvent: function (evtNm, id) {
if (id) {
let listeners = this.map.listeners_[evtNm];
for (let i in listeners) {
let listenter = listeners[i];
if (listenter.id == id) {
listeners.splice(i, 1);
}
}
}
},
/**
* [사용]마커 그리기 추가
*/
drawMarker: function (layer, callback) {
let map = this.map;
let drawSource = layer.getSource();
let draw = new Draw({
type: 'Point',
source: drawSource,
condition: function (evt) {
return true;
},
});
map.addInteraction(draw);
draw.on('drawend', function (event) {
callback(event);
map.removeInteraction(draw);
});
},
/**
* [사용]피처 삭제
*/
deleteFeature: function (layerId, featureId) {
if (!layerId || !featureId) return;
let layer = this.getLayerById(layerId);
if (layer != null) {
let feature = layer.getSource().getFeatureById(featureId);
if (feature != null) {
layer.getSource().removeFeature(feature);
}
}
},
/**
* [사용]레이어
*/
fitExtent: function (extent, zoom) {
let map = this.map;
let mapSize = map.getSize();
if (!zoom) {
zoom = this.getZoom();
}
if (extent) {
//map.getView().fit(extent, { size: [mapSize[0], mapSize[1]] });
this.map.getView().fit(extent, {
maxZoom: 18, // 너무 확대되지 않도록 최대 줌 제한
duration: 500, // 부드러운 애니메이션 효과 (밀리초 단위)
});
}
this.setZoom(zoom);
},
getGeomLineOfFeature: function (feature) {
if (!feature) {
return;
}
let geometry = feature.getGeometry();
let type = geometry.getType();
if (!(type == 'LineString' || type == 'MultiLineString')) {
return;
}
let geomLienString = geometry;
if (type != 'LineString') {
//lienString = geometry.getLineString();
}
return geomLienString;
},
getCenterGeomLineOfFeature: function (feature) {
if (!feature) {
return;
}
let center = [];
let geomLienString = this.getGeomLineOfFeature(feature);
let coords = geomLienString.getCoordinates();
let coordCnt = coords.length;
if (coordCnt == 1) {
coords = coords[0];
}
//라인 한개
if (coordCnt == 2) {
return geomLienString;
}
//멀티라인
let centerCnt = Math.floor(coordCnt / 2);
let lineCoordinates = [];
lineCoordinates.push(coords[centerCnt - 1]);
lineCoordinates.push(coords[centerCnt]);
geomLienString = new LinearRing(lineCoordinates);
return geomLienString;
},
getCenterOfLineGeom: function (geom) {
let center = [];
let extent = geom.getExtent();
let x = extent[0] + (extent[2] - extent[0]) / 2;
let y = extent[1] + (extent[3] - extent[1]) / 2;
center.push(x);
center.push(y);
return center;
},
/**
* 1 -> 01 두자리수로 변환
*/
fillZeroToZoomLevel: function (n, digits) {
let zero = '';
n = n.toString();
if (digits > n.length) {
for (let i = 0; digits - n.length > i; i++) {
zero += '0';
}
}
return zero + n;
},
convertGeoJsonToFeature: function (data, dataProjection) {
let format = new GeoSJON();
let feature = format.readFeature(data, {
dataProjection: dataProjection || GisApp.TO_DATA_PROJ,
featureProjection: GisApp.TO_PROJ,
});
return feature;
},
convertGeoJsonToFeatures: function (data, dataProjection) {
let format = new GeoSJON();
const features = [];
for (let obj of data) {
let feature = null;
try {
feature = format.readFeature(obj, {
dataProjection: dataProjection || GisApp.TO_DATA_PROJ,
featureProjection: GisApp.TO_PROJ,
});
} catch (error) {
console.log(obj);
}
features.push(feature);
}
return features;
},
/**
* [사용]WKT포맷 Geometry로 feature 생성
*/
convertWktToFeature: function (wtkGeom, dataProjection) {
let format = new WKT();
let feature = format.readFeature(wtkGeom, {
dataProjection: dataProjection || GisApp.TO_DATA_PROJ,
featureProjection: GisApp.TO_PROJ,
});
return feature;
},
/**
* [사용]geometry를 WKT포맷으로 변경
*/
convertGeomToWkt: function (geometry) {
let format = new WKT();
let wktGeom = format.writeGeometry(geometry, {
dataProjection: GisApp.TO_DATA_PROJ,
featureProjection: GisApp.TO_PROJ,
});
return wktGeom;
},
/**
* [사용]레이어 전체 표시 컨트롤
*/
setAllLayersOnMap: function (visible) {
this.map.getLayers().forEach(function (el) {
if (el !== null && el.get('id') !== null) {
let layerId = el.get('id');
if ('base' !== layerId && 'satellite' !== layerId && 'minimap-base' !== layerId) {
el.setVisible(visible);
}
}
});
},
fitExtentZoomFeatures: function (features, zoom, paddingX = 0, paddingY = 0) {
if (!features.length) return;
// Feature들의 경계를 계산하여 extent 생성
const extent = this.getExtentFeatures(features);
if (zoom) {
//const paddingX = 0; // X축 이동량 (양수는 오른쪽, 음수는 왼쪽)
//const paddingY = -10000; // Y축 이동량 (양수는 아래쪽, 음수는 위쪽)
extent[0] -= paddingX;
extent[1] -= paddingY;
this.fitExtent(extent, zoom);
} else {
this.map.getView().fit(extent, {
maxZoom: 18, // 너무 확대되지 않도록 최대 줌 제한
duration: 500, // 부드러운 애니메이션 효과 (밀리초 단위)
});
}
},
getExtentFeatures: function (features) {
if (!features.length) return;
let extent = [];
const allCoordinates = features
.map(feature => {
const geometry = feature.getGeometry();
if (!geometry || typeof geometry.getCoordinates !== 'function') {
return null; // geometry가 없거나 getCoordinates() 메서드가 없으면 null 반환
}
return geometry.getCoordinates();
})
.filter(coords => coords !== null);
function extractCoordinatePairs(arr) {
let result = [];
function recursiveFlatten(subArray) {
if (!Array.isArray(subArray)) return;
for (let item of subArray) {
if (Array.isArray(item) && item.length === 2 && typeof item[0] === 'number' && typeof item[1] === 'number') {
// [숫자, 숫자] 형태의 배열이면 추가
result.push(item);
} else {
// 그렇지 않다면 재귀 호출
recursiveFlatten(item);
}
}
}
recursiveFlatten(arr);
return result;
}
const flattenCoord = extractCoordinatePairs(allCoordinates);
extent = boundingExtent(flattenCoord);
return extent;
},
};
/**
* [사용]피처 레이어 검색
*/
Feature.prototype.getLayer = function () {
let map = GisApp.Module.core.map;
let this_ = this,
layer_;
let sameFeature = function (feature) {
return this_ === feature ? true : false;
};
map.getLayers().forEach(function (layer) {
let source = layer.getSource();
if (source instanceof VectorSource) {
let features = source.getFeatures();
if (features.length > 0) {
let found = features.some(sameFeature);
if (found) {
layer_ = layer;
}
}
}
});
return layer_;
};
export default GisApp;
@@ -0,0 +1,521 @@
import GisApp from './gis.core';
import Draw from 'ol/interaction/Draw.js';
import Modify from 'ol/interaction/Modify.js';
import Collection from 'ol/Collection.js';
import Snap from 'ol/interaction/Snap.js';
import { click, always } from 'ol/events/condition';
import UndoRedo from 'ol-ext/interaction/UndoRedo.js';
import Transform from 'ol-ext/interaction/Transform.js';
import { gAlert } from './util';
import VectorSource from 'ol/source/Vector.js';
import VectorLayer from 'ol/layer/Vector';
const GisOlEditLayer = function (options) {
this.initialize(options);
};
GisOlEditLayer.prototype = {
defaults: {},
editFlag: false,
draw: null,
modify: null,
snap: null,
undoredo: null,
transform: null,
drawType: {
point: 'Point',
line: 'MultiLineString',
poligon: 'MultiPolygon',
},
selectedLayerId: null, //셀렉트 박스에서 선택된 레이어 ID
targetElement: null,
editFeatureInfo: {},
addFeatureNo: 1,
initialize: function (options) {
Object.assign(this.defaults, options);
},
/**
* [공통] Interaction 추가
*/
addInteration: function () {
this.setUndoInteraction();
},
/**
* [공통] Interaction 삭제
*/
removeInteraction: function () {
this.removeDraw();
this.removeModify();
this.removeSnap();
this.clearUndoRedoFeature();
},
/**
* [공통] Interaction Active 컨트롤
*/
setInteractionActive: function (active) {
this.setDrawActive(active);
this.setModifyActive(active);
this.setActiveUndo(active);
},
/**
* [그리기] 기능 추가
*/
setDraw: function (lyrId) {
if (!lyrId) return '레이어를 선택해주십시오.';
const core = GisApp.Module.core;
const layer = core.getLayerById(lyrId);
const layerCode = GisApp.layerCode[lyrId];
if (!layer) return '트리에서 레이어 체크 후 추가 가능합니다.';
const lyrTy = layerCode.gobjTy;
//초기화
this.removeModify();
this.removeTransform();
if (GisApp.Module.select) {
GisApp.Module.select.setActive(false);
}
const originalSource = layer.getSource();
const drawSource = new VectorSource({
features: originalSource.getFeatures(), // 기존 feature들 복사
});
// 새로운 임시 레이어 추가 (draw 전용)
const drawLayer = new VectorLayer({
source: drawSource,
minZoom: 1, // 이 레이어만 minZoom을 1로 설정
style: layer.getStyle(),
});
core.map.addLayer(drawLayer);
//그리기 기능 추가
const me = this;
if (me.draw) {
core.map.removeInteraction(me.draw);
}
const drawType = this.drawType[lyrTy];
this.draw = new Draw({
type: drawType,
source: drawSource,
condition: function (evt) {
return true;
},
});
core.map.addInteraction(this.draw);
//그리기 기능 이벤트
this.draw.on('drawstart', function (event) {});
const drawFeatureCallback = function (event) {
me.setActiveUndo(true);
const drawFeature = event.feature;
drawFeature.setId(lyrId + '_feature_' + me.addFeatureNo++);
const properties = {};
properties['lyr_id'] = lyrId;
drawFeature.setProperties(properties);
me.setAddFeatureUndo(layer, drawFeature);
me.setActiveUndo(false);
};
this.draw.on('drawend', function (event) {
drawFeatureCallback(event);
});
/*drawSource.listeners_.addfeature = null;
let addfeatureCallback = function(event){
}
drawSource.on('addfeature', addfeatureCallback);*/
},
/**
* [그리기] 기능 제거
*/
removeDraw: function () {
let core = GisApp.Module.core;
core.map.removeInteraction(this.draw);
if (GisApp.Module.select) {
GisApp.Module.select.setActive(true);
}
},
/**
* [그리기] 기능 활성화
*/
setDrawActive: function (active) {
if (this.draw) {
this.draw.setActive(active);
if (!active) {
/* $('#selectEditLayer').css('display', 'none');
$('.btn_edit_add').removeClass('active'); */
}
}
},
/**
* [히스토리] UndoInteraction 추가
*/
setUndoInteraction: function () {
let core = GisApp.Module.core;
if (!this.undoredo) {
this.undoredo = new UndoRedo();
core.map.addInteraction(this.undoredo);
}
},
/**
* [히스토리] UndoInteraction 제거
*/
removeUndoInteraction: function () {
let core = GisApp.Module.core;
if (this.undoredo) {
core.map.removeInteraction(this.undoredo);
this.undoredo = null;
}
},
/**
* [히스토리] UndoInteraction 활성화
*/
setActiveUndo: function (active) {
if (this.undoredo) {
this.undoredo.setActive(active);
}
},
/**
* [히스토리] 피처 추가
*/
setAddFeatureUndo: function (layer, addFeature) {
if (this.undoredo) {
let obj = {};
obj.target = layer.getSource();
obj.feature = addFeature;
obj.type = 'addfeature';
obj.feature.set('uploadType', 'addFeature');
this.undoredo._onAddRemove(obj);
}
},
/**
* [히스토리] 피처 삭제
*/
setRemoveFeatureUndo: function (layer, removeFeature) {
if (this.undoredo) {
let obj = {};
obj.target = layer.getSource();
obj.feature = removeFeature;
obj.type = 'removefeature';
if (obj.feature.get('uploadType') !== 'addFeature') {
obj.feature.set('uploadType', 'removeFeature');
}
this.undoredo._onAddRemove(obj);
}
},
/**
* [히스토리] 이전
*/
undo: function () {
if (this.undoredo) {
if (this.undoredo.hasUndo() === 0) {
gAlert('INFO', 'NO_PREV_HISTORY');
return;
}
this.setActiveUndo(true);
this.undoredo.undo();
this.setActiveUndo(false);
}
},
/**
* [히스토리] 다음
*/
redo: function () {
if (this.undoredo) {
if (this.undoredo.hasRedo() === 0) {
gAlert('INFO', 'NO_NEXT_HISTORY');
return;
}
this.setActiveUndo(true);
this.undoredo.redo();
this.setActiveUndo(false);
}
},
/**
* [히스토리] 초기화
*/
clearUndoRedo: function () {
if (this.undoredo) {
const undoStack = this.undoredo._undoStack.array_;
for (let i in undoStack) {
const undo = undoStack[i];
if (undo.feature) {
undo.feature.set('uploadType', null);
}
}
this.undoredo.clear();
}
},
/**
* [히스토리] 피처 초기화
*/
clearUndoRedoFeature: function () {
if (this.undoredo) {
this.setActiveUndo(true);
const undoStack = [...this.undoredo._undoStack.array_];
for (let i in undoStack) {
const undo = undoStack[i];
if (undo.feature) {
this.undoredo.undo();
}
}
this.setActiveUndo(false);
this.undoredo.clear();
}
},
/**
* [삭제] 피처 삭제
*/
deleteFeature: function () {
//초기화
this.removeDraw();
this.removeModify();
this.removeTransform();
//삭제 기능 실행
let me = this;
let core = GisApp.Module.core;
let select = GisApp.Module.select;
select.getSelectedFeatures().forEach(function (selectedFeature) {
let selectedFeatureId = selectedFeature.getId();
let layers = core.map.getLayers();
for (let i in layers.array_) {
let layer = layers.array_[i];
if (layer.get('mdfcnYn') === 'Y' && select.getVisible(layer)) {
layer
.getSource()
.getFeatures()
.forEach(function (feature) {
let featureId = feature.getId();
if (selectedFeatureId === featureId) {
me.setActiveUndo(true);
feature.setProperties({ lyr_id: layer.get('id') });
layer.getSource().removeFeature(feature);
me.setRemoveFeatureUndo(layer, feature);
me.setActiveUndo(false);
}
});
}
}
});
},
/**
* [수정] 피처 수정 기능 셋팅
*/
setModify: function () {
//초기화
this.removeDraw();
this.removeTransform();
//수정기능 추가
let me = this;
let core = GisApp.Module.core;
let select = GisApp.Module.select;
let selectedFeatures = select.getSelectedFeatures();
this.modify = new Modify({
features: selectedFeatures,
});
core.map.addInteraction(this.modify);
select.selectInteraction.on('change:active', function () {
selectedFeatures.forEach(function (each) {
selectedFeatures.remove(each);
});
});
this.modify.on('modifystart', function (event) {});
this.modify.on('modifyend', function (event) {
let features = event.features.getArray();
for (let i = 0; i < features.length; i++) {
let feature = features[i];
let layer = select.selectInteraction.getLayer(feature);
feature.setProperties({ lyr_id: layer.get('id') });
}
});
},
/**
* [수정] 피처 수정 기능 제거
*/
removeModify: function () {
if (this.modify) {
let core = GisApp.Module.core;
core.map.removeInteraction(this.modify);
this.modify = null;
}
},
/**
* [수정] 기능 활성화
*/
setModifyActive: function (active) {
if (this.modify) {
this.modify.setActive(active);
}
},
/**
* [크기조절] 피처 크기조절 기능 셋팅
*/
setTransform: function () {
//초기화
this.removeDraw();
this.removeModify();
let me = this;
let core = GisApp.Module.core;
let select = GisApp.Module.select;
this.transform = new Transform({
enableRotatedTransform: false,
addCondition: click,
filter: function (f, l) {
return f.getGeometry().getType() !== 'Point';
},
// layers: [vector],
hitTolerance: select.defaults.hitTolerance,
translateFeature: true,
scale: true,
rotate: true,
keepAspectRatio: always,
translate: true,
stretch: true,
});
this.transform.on(['rotateend', 'translateend', 'scaleend'], function (event) {
let features = event.features.getArray();
for (let i = 0; i < features.length; i++) {
let feature = features[i];
let layer = feature.getLayer();
feature.setProperties({ lyr_id: layer.get('id') });
}
});
core.map.addInteraction(this.transform);
},
/**
* [크기조절] 피처 크기조절 기능 제거
*/
removeTransform: function () {
if (this.transform) {
let core = GisApp.Module.core;
core.map.removeInteraction(this.transform);
this.transform = null;
}
},
/**
* [크기조절] 피처 크기조절 기능 활성화
*/
setTransformActive: function (active) {
if (this.transform) {
this.transform.setActive(active);
}
},
/**
* [스냅] 기능 추가
*/
setSnap: function (active) {
//초기화
this.removeSnap();
//스냅 기능 추가
let core = GisApp.Module.core;
let select = GisApp.Module.select;
core.map.removeInteraction(this.snap);
let features = [];
let layers = core.map.getLayers();
for (let i in layers.array_) {
let layer = layers.array_[i];
if (select.getVisible(layer) && layer.get('type') !== 'base') {
if (layer.getSource().getFeatures().length > 0) {
features = features.concat(layer.getSource().getFeatures());
}
}
}
let snapCollection = new Collection(features);
this.snap = new Snap({
features: snapCollection,
pixelTolerance: 30,
});
core.map.addInteraction(this.snap);
},
/**
* [스냅] 기능 제거
*/
removeSnap: function () {
if (this.snap) {
let core = GisApp.Module.core;
core.map.removeInteraction(this.snap);
this.snap = null;
}
},
/**
* [스냅] 스냅에 피처 추가
*/
addSnap: function (layers) {
let me = this;
if (me.snap) {
for (let i in layers) {
let source = layers[i].getSource();
source.listeners_.addfeature = null;
source.on('addfeature', function (event) {
me.snap.addFeature(event.feature);
});
}
}
},
/**
* [스냅] 기능 활성화
*/
setSnapActive: function (active) {
if (this.snap) {
this.snap.setActive(active);
}
},
/**
* [저장] 피처 수정 정보 셋팅
*/
setEditFeatureInfo: function (flag, feature) {
const featureId = feature.getId();
const properties = feature.getProperties();
//신규 추가 후 삭제시에 editFeatureInfo에서 제거
if (properties.uploadType === 'addFeature' && flag === 'delete') {
delete this.editFeatureInfo[featureId];
return;
}
//좌표값 WKT포멧으로 변경
const pGeom = GisApp.Module.core.convertFeatureGeomToWKT(feature);
const param = {
flag,
...properties,
geom: pGeom,
};
this.editFeatureInfo[featureId] = param;
},
getEditFeatureInfo: function () {
const undoStack = this.undoredo._undoStack.array_;
this.editFeatureInfo = {};
for (let i in undoStack) {
const undo = undoStack[i];
if (undo.feature) {
const feature = undo.feature;
const properties = feature.getProperties();
if (properties.uploadType === 'addFeature') {
this.setEditFeatureInfo('insert', feature);
} else if (properties.uploadType === 'removeFeature') {
this.setEditFeatureInfo('delete', feature);
} else if (undo.type === 'changegeometry') {
this.setEditFeatureInfo('update', feature);
}
}
}
return this.editFeatureInfo;
},
};
export default GisOlEditLayer;
@@ -0,0 +1,557 @@
import Feature from 'ol/Feature.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import Point from 'ol/geom/Point.js';
import VectorLayer from 'ol/layer/Vector';
import VectorImageLayer from 'ol/layer/VectorImage.js';
import { all, bbox } from 'ol/loadingstrategy';
import { transform } from 'ol/proj';
import Cluster from 'ol/source/Cluster.js';
import VectorSource from 'ol/source/Vector.js';
import VectorTile from 'ol/source/VectorTile.js';
import CircleStyle from 'ol/style/Circle.js';
import Fill from 'ol/style/Fill.js';
import Icon from 'ol/style/Icon.js';
import Stroke from 'ol/style/Stroke.js';
import Style from 'ol/style/Style.js';
import Text from 'ol/style/Text.js';
import { createXYZ } from 'ol/tilegrid';
import GisApp from './gis.core.js';
import GisOlStyle from './gis.style.js';
import Tile from 'ol/layer/Tile.js';
import TileWMS from 'ol/source/TileWMS.js';
import gisCore from './gis.core.js';
/**
* GisApp.layer
* @requires gis.core.js
* @requires gis.style.js
*/
const GisOlLayer = function (options) {
this.initialize(options);
};
GisOlLayer.prototype = {
layers: [],
cesiumLayers: [],
defaults: {},
storageNm: 'hasudo:',
defaultLayerOptions: {
name: '',
zIndex: 2,
opacity: 1,
flterSql: null,
minZoom: '15',
},
initialize: function (options) {
Object.assign(this.defaults, options);
this.gisStyle = new GisOlStyle();
GisApp.Module.style = this.gisStyle;
},
gisStyle: null,
/**
* [사용]빈 레이어 생성
*/
makeLayer: function (options) {
let layerOptions = {
...this.defaultLayerOptions,
...options,
minZoom: options?.minViewSize ? options?.minViewSize : options?.minZoom,
zIndex: options?.drawOrdr ? options?.drawOrdr : 1,
};
let returnLayer = new VectorImageLayer(layerOptions);
this.layers.push(returnLayer);
return returnLayer;
},
/**
* gisStyle 반환
*/
getGisStyle: function () {
return this.gisStyle;
},
/**
* [사용]WFS 벡터 소스 반환
*/
getVectorSourceGeoserver: function (options) {
const url = GisApp.GEOSERVER_URL;
const toProj = GisApp.TO_PROJ;
const lyrTyNm = options.lyrTyNm;
const params = {};
params['TYPENAME'] = this.storageNm + lyrTyNm;
params['SERVICE'] = 'WFS';
params['VERSION'] = '1.1.0';
params['REQUEST'] = 'GetFeature';
params['SRS'] = toProj;
params['OUTPUTFORMAT'] = 'application/json';
if (options?.flterSql && options?.flterSql?.trim() !== 'null') {
params['CQL_FILTER'] = options?.flterSql;
}
const strParams = new URLSearchParams(params).toString();
let source = new VectorSource({
format: new GeoJSON(),
url: function (extent) {
return url + '?' + strParams;
},
strategy: all,
});
return source;
},
/**
* [사용]WFS 벡터 소스 반환
*/
getVectorTileSource: function (options) {
let url = GisApp.GEOSERVER_URL;
let toProj = GisApp.TO_PROJ;
let params = {};
params['TYPENAME'] = this.storageNm + options.typeName;
params['SERVICE'] = 'WFS';
params['VERSION'] = '1.1.0';
params['REQUEST'] = 'GetFeature';
params['SRS'] = toProj;
params['OUTPUTFORMAT'] = 'application/json';
let strParams = new URLSearchParams(params).toString();
let source = new VectorTile({
format: new GeoJSON(),
tileUrlFunction: function (tileCoord, pixelRatio, projection) {
let filterStr = '&CQL_FILTER=bbox(GEOM,' + source.getTileGrid().getTileCoordExtent(tileCoord).join(',') + ",'" + projection.getCode() + "')";
if (options.flterSql) {
filterStr += ' AND ' + options.flterSql;
}
let paramWithUrl = url + '?' + strParams + filterStr;
return paramWithUrl;
},
tileGrid: createXYZ,
});
return source;
},
/**
* [사용]WFS 레이어 생성
*/
getWfsLayer: function (option) {
option = option || {};
//레이어 생성
let returnLayer = this.makeLayer(option);
//레이어 소스 생성
let source = this.getVectorSourceGeoserver(option);
returnLayer.setSource(source);
//스타일 추가
returnLayer.setStyle(this.gisStyle.getLayerStyleFunction(returnLayer, option));
returnLayer.set('id', option.id + '');
return returnLayer;
},
/**
* [사용]WMS 레이어 생성
*/
getWmsLayer: function (options) {
let layerOptions = Object.assign({}, this.defaultLayerOptions, options);
let url = GisApp.GEOSERVER_WMS_URL;
let toProj = GisApp.TO_PROJ;
const lyrTyNm = options.lyrTyNm;
let params = {};
params['LAYERS'] = this.storageNm + lyrTyNm;
params['SERVICE'] = 'WMS';
params['VERSION'] = '1.1.1';
params['REQUEST'] = 'GetMap';
params['SRS'] = toProj;
params['FORMAT'] = 'image/png';
params['TILED'] = true;
const wmsLayer = new Tile({
...layerOptions,
id: layerOptions?.id,
source: new TileWMS({
url,
params,
serverType: 'geoserver',
}),
});
wmsLayer.setZIndex(options.draw0rdr);
wmsLayer.setMinZoom(options.minViewSize);
this.layers.push(wmsLayer);
return wmsLayer;
},
/**
* [사용]WFS 레이어 생성
*/
getWfsEmptyLayer: function (option) {
option = option || {};
//레이어 생성
let returnLayer = this.makeLayer(option);
let vectorSource = new VectorSource({
strategy: all,
});
returnLayer.setSource(vectorSource);
//스타일 추가
returnLayer.setStyle(this.gisStyle.getLayerStyle(returnLayer, option));
returnLayer.set('id', option.id + '');
let map = GisApp.Module.core.map;
map.addLayer(returnLayer);
return returnLayer;
},
/**
* 피처 리스트로 레이어 생성
*/
getLayerByFeature: function (feature, layerOptions, layerStyle) {
if (!layerOptions || !layerOptions.id) {
return null;
}
let vectorSource = new VectorSource({
strategy: all,
});
if (feature) {
if (Object.prototype.toString.call(feature).slice(8, -1).indexOf('Object') > -1) {
vectorSource.addFeature(feature);
} else {
vectorSource.addFeatures(feature);
}
}
GisApp.Module.core.deleteLayer(layerOptions.id);
layerOptions.source = vectorSource;
let layer = new VectorImageLayer(layerOptions);
layer.setStyle(layerStyle);
layer.set('id', layerOptions.id + '');
return layer;
},
/**
* 피처 하이라이트
*/
highLightFeature: function (hlFeature, layerOptions, styleOptions, customStyleFunction) {
const color = styleOptions.color || 'rgba(135, 116, 243, 0.4)';
const width = styleOptions.width || 10;
const featureKey = styleOptions.featureKey || '';
const fitExtent = layerOptions.fitExtent || 'true';
const styleFunction = function (feature, resolution) {
const featureStyle = feature.getStyle();
if (featureStyle) return null;
const style = new Style({
image: new CircleStyle({
fill: new Fill({
color: color,
}),
radius: width,
}),
stroke: new Stroke({
color: color,
width: width,
}),
});
const getFeatureText = function (feature) {
if (featureKey) {
let text = feature.get(featureKey);
if (text) return text;
}
return '';
};
const text = new Text({
stroke: new Stroke({
color: 'white',
width: 2,
}),
fill: new Fill({
color: 'black',
}),
font: '12px Verdana',
text: getFeatureText(feature),
});
style.setText(text);
return style;
};
let actualStyleFunction = customStyleFunction || styleFunction;
GisApp.Module.core.deleteLayer(layerOptions.id);
let layer = this.getLayerByFeature(hlFeature, layerOptions, actualStyleFunction);
if (hlFeature && hlFeature.length > 0) {
let map = GisApp.Module.core.map;
map.addLayer(layer);
if (fitExtent === 'true') {
GisApp.Module.core.fitExtent(layer.getSource().getExtent());
}
}
return layer;
},
/**
* 마커 레이어 맵에 추가
*/
addMarkerLayer: function (lat, lon, fromProj, layerOptions) {
let p = transform([Number(lon), Number(lat)], fromProj, GisApp.TO_PROJ);
let feature = new Feature({
geometry: new Point(p),
});
let layer = this.getMarkerLayer(layerOptions);
layer.getSource().addFeature(feature);
let map = GisApp.Module.core.map;
map.addLayer(layer);
GisApp.Module.core.fitExtent(feature.getGeometry().getExtent());
return layer;
},
/**
* 마커 레이어 생성
*/
getMarkerLayer: function (layerOptions) {
let imgSrc = GisApp.CONTEXT_PATH + 'images/marker/marker-icon.png';
let style = new Style({
image: new Icon({
anchor: [0.5, 46],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: imgSrc,
}),
});
let layer = this.getLayerByFeature(null, layerOptions, style);
return layer;
},
/**
* 피처 생성
*/
getFeatureCoord: function (coord) {
let feature = new Feature({
geometry: new Point(coord),
});
return feature;
},
/**
* 피처 생성(lat,lon)
*/
getFeatureLonLat: function (lat, lon) {
let p = transform([Number(lon), Number(lat)], GisApp.FROM_PROJ, GisApp.TO_PROJ);
let feature = new Feature({
geometry: new Point(p),
});
return feature;
},
addPolygonLayer: function (features, option) {
const map = GisApp.Module.core.map;
option = Object.assign(
{
gobjTy: 'lineLabelPoligon',
minZoom: option?.minZoom || '10',
lblAtrbNm: option?.lblAtrbNm || 'fclty_nm',
lnWdth: option?.lnWdth || '2',
lnColor: option?.lnColor || 'rgba(0, 0, 0, 1)',
polgnColor: option?.polgnColor || 'rgba(255, 0, 0, 0.2)',
clickPsblYn: option?.clickPsblYn || 'N',
fondNm: '18px "맑은 고딕",Malgun Gothic',
},
option
);
features[0].set('overLayId', option?.overlayId || null);
features[0].set('html', option?.html || null);
//레이어 생성
let vectorLayer = this.makeLayer(option);
//레이어 소스 생성
const vectorSource = new VectorSource({});
vectorSource.addFeatures(features);
vectorLayer.setSource(vectorSource);
//스타일 추가
vectorLayer.setStyle(this.gisStyle.getLayerStyleFunction(vectorLayer, option));
vectorLayer.set('id', option?.id + '');
GisApp.Module.core.deleteLayer(option?.id);
map.addLayer(vectorLayer);
},
addLayerByLyrTyNm: function (id, lyrTyNm, addQurryFlter) {
const map = GisApp.Module.core.map;
const layerCode = GisApp.layerCode;
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
matched['id'] = id;
let layer = GisApp.Module.core.getLayerById(matched?.id);
if (layer) {
layer.setVisible(true);
} else {
const queryFlter = layerCode?.flterSql || '' + ' ' + (addQurryFlter || '');
matched['flterSql'] = queryFlter;
layer = this.getWfsLayer(matched);
GisApp.Module.core.deleteLayer(id);
map.addLayer(layer);
}
},
/**
* 클러스터 레이어 맵에 추가
*/
addClusterLayer: function (features, { layerId, color }) {
const map = GisApp.Module.core.map;
const vectorSource = new VectorSource({});
vectorSource.addFeatures(features);
const clusterLayerId = layerId + 'Cluster';
const vectorLayerId = layerId + 'Vector';
// 클러스터 레이어 생성
const clusterLayer = new VectorLayer({
id: clusterLayerId,
source: new Cluster({
distance: 100, // 클러스터링 거리
source: vectorSource,
geometryFunction: function (feature) {
let geometry = feature.getGeometry();
let type = geometry.getType();
let coordinates;
if (type === 'Point') {
coordinates = geometry.getCoordinates();
} else if (type === 'LineString' || type === 'MultiLineString') {
let totalX = 0;
let totalY = 0;
let totalPoints = 0;
geometry.getCoordinates().forEach(line => {
line.forEach(point => {
totalX += point[0];
totalY += point[1];
totalPoints++;
});
});
coordinates = [totalX / totalPoints, totalY / totalPoints];
}
return new Point(coordinates);
},
}),
style: feature => {
const clusterSize = feature.get('features').length;
return new Style({
image: new CircleStyle({
radius: 10,
fill: new Fill({ color }),
}),
stroke: new Stroke({
color,
width: 10,
}),
text: new Text({
text: clusterSize.toString(),
fill: new Fill({
color: 'white',
}),
stroke: new Stroke({
color: 'black',
width: 4,
}),
}),
});
},
});
const vectorLayer = new VectorLayer({
id: vectorLayerId,
zIndex: 1,
source: vectorSource,
style: new Style({
image: new CircleStyle({
radius: 10,
fill: new Fill({ color }),
}),
stroke: new Stroke({
color,
width: 10,
}),
}),
});
GisApp.Module.core.deleteLayer(clusterLayerId);
GisApp.Module.core.deleteLayer(vectorLayerId);
map.addLayer(clusterLayer);
map.addLayer(vectorLayer);
function handleZoomEnd() {
const findClusterLayer = map
?.getLayers()
?.getArray()
.find(function (layer) {
return layer.get('id') === clusterLayerId;
});
const findVectorLayer = map
?.getLayers()
?.getArray()
.find(function (layer) {
return layer.get('id') === vectorLayerId;
});
if (!findClusterLayer) {
GisApp.Module.core.deleteLayer(vectorLayerId);
}
if (!findVectorLayer) {
GisApp.Module.core.deleteLayer(vectorLayerId);
}
const zoomLevel = map.getView().getZoom();
if (zoomLevel >= 18) {
vectorLayer.setVisible(true);
clusterLayer.setVisible(false);
} else {
clusterLayer.setVisible(true);
vectorLayer.setVisible(false);
}
if (!findClusterLayer && !findVectorLayer) {
map.getView().un('change:resolution', handleZoomEnd);
}
}
// 줌 이벤트 핸들러 등록
map.getView().un('change:resolution', handleZoomEnd);
map.getView().on('change:resolution', handleZoomEnd);
handleZoomEnd();
},
setLayersToCesium: function (ol3dInstance) {
let me = this;
//const { Cesium } = window;
const cesiumScene = ol3dInstance.getCesiumScene(); // Cesium Scene 가져오기
me.cesiumLayers.forEach(cesiumLayer => {
cesiumScene.imageryLayers.remove(cesiumLayer);
});
const layers = GisApp.Module.core.map
.getAllLayers()
.filter(layer => layer.get('type') !== 'base')
.sort((a, b) => {
const orderA = a.getProperties().drawOrdr || 0;
const orderB = b.getProperties().drawOrdr || 0;
return orderA - orderB; // 낮은 값이 먼저 오도록 정렬
});
layers.forEach(layer => {
const properties = layer.getProperties();
let cesiumLayer = null;
if (properties?.lyrTyNm) {
const visible = layer.getVisible();
const lyrTyNm = properties.lyrTyNm;
// OpenLayers에서 레이어의 가시성 확인
const { Cesium } = window;
const wmsUrl = GisApp.GEOSERVER_WMS_URL;
const wmsProvider = new Cesium.WebMapServiceImageryProvider({
url: wmsUrl,
layers: GisApp.Module.layer.storageNm + lyrTyNm, // WMS 레이어 이름
parameters: {
service: 'WMS',
version: '1.1.1',
request: 'GetMap',
format: 'image/png',
transparent: true,
styles: lyrTyNm,
},
});
cesiumLayer = cesiumScene.imageryLayers.addImageryProvider(wmsProvider);
cesiumLayer['properties'] = properties;
me.cesiumLayers.push(cesiumLayer);
if (properties?.visible) {
const zoomLevel = getOpenLayersZoom(cesiumScene);
cesiumLayer.show = zoomLevel >= Number(properties?.minZoom);
} else {
cesiumLayer.show = false;
}
}
});
if (me.cesiumLayers.length > 0) {
cesiumScene._moveEndListener = () => {
const zoomLevel = getOpenLayersZoom(cesiumScene); // 대략적인 줌 계산
me.cesiumLayers.forEach(cesiumLayer => {
const properties = cesiumLayer.properties;
if (properties?.visible) {
console.log('zoomLevel >= minZoom==', properties?.lyrTyNm, zoomLevel >= properties?.minZoom);
cesiumLayer.show = zoomLevel >= Number(properties?.minZoom);
}
});
};
cesiumScene.camera.moveEnd.addEventListener(cesiumScene._moveEndListener);
}
},
};
function getOpenLayersZoom(cesiumScene) {
const cameraHeight = cesiumScene.camera.positionCartographic.height;
const maxResolution = 156543.03392804097; // OpenLayers 기본 해상도 (Google Mercator 기준)
const tileSize = 256; // 기본 타일 크기
return Math.ceil(Math.log2(maxResolution / (cameraHeight / tileSize)));
}
export default GisOlLayer;
@@ -0,0 +1,204 @@
import Style from 'ol/style/Style';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import Fill from 'ol/style/Fill.js';
import Stroke from 'ol/style/Stroke';
import CircleStyle from 'ol/style/Circle';
import { getLength, getArea } from 'ol/sphere';
import Draw from 'ol/interaction/Draw';
import Polygon from 'ol/geom/Polygon';
import LineString from 'ol/geom/LineString';
import { unByKey } from 'ol/Observable';
import Overlay from 'ol/Overlay';
import GisApp from './gis.core';
//import DOMPurify from 'dompurify';
const GisOlMeasure = function (options) {
GisApp.measure = {};
GisApp.measure.helpTooltipElement = null;
GisApp.measure.helpTooltip = null;
GisApp.measure.sketch = null;
GisApp.measure.measureTooltipElement = null;
GisApp.measure.measureTooltip = null;
GisApp.measure.continueLineMsg = '선 그리기 종료를 위해 마우스를 더블클릭하세요.';
GisApp.measure.Polygon = 'Polygon';
GisApp.measure.LineString = 'LineString';
this.initialize(options);
};
GisOlMeasure.prototype = {
map: null,
draw: null,
vector: null,
type: null,
initialize: function (map) {
let me = this;
let source = new VectorSource();
let vector = new VectorLayer({
source: source,
style: new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
stroke: new Stroke({
color: '#ffcc33',
width: 2,
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: '#ffcc33',
}),
}),
}),
});
me.map = map;
me.vector = vector;
me.map.addLayer(vector);
me.map.removeInteraction(this.draw);
},
pointerMoveHandler: function (evt) {
if (evt.dragging) {
return;
}
let helpMsg = '시작점을 선택하세요';
if (GisApp.measure.sketch) {
helpMsg = GisApp.measure.continueLineMsg;
}
if (GisApp.measure.helpTooltipElement) {
//GisApp.measure.helpTooltipElement.innerHTML = DOMPurify.sanitize(helpMsg);
GisApp.measure.helpTooltipElement.innerHTML = helpMsg;
GisApp.measure.helpTooltip.setPosition(evt.coordinate);
GisApp.measure.helpTooltipElement.classList.remove('hidden');
}
},
formatLength: function (line) {
let length = getLength(line);
let output;
if (length > 1000) {
output = Math.round((length / 1000) * 100) / 100 + ' ' + 'km';
} else {
output = Math.round(length * 100) / 100 + ' ' + 'm';
}
return output;
},
formatArea: function (polygon) {
let area = getArea(polygon);
let output;
if (area > 10000) {
output = Math.round((area / 1000000) * 100) / 100 + ' ' + 'km<sup>2</sup>';
} else {
output = Math.round(area * 100) / 100 + ' ' + 'm<sup>2</sup>';
}
return output;
},
addInteraction: function (type) {
let me = this;
me.map.removeInteraction(me.draw);
me.map.on('pointermove', me.pointerMoveHandler);
me.type = type;
let source = me.vector.getSource();
me.draw = new Draw({
source: source,
type: type,
style: new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.5)',
lineDash: [10, 10],
width: 2,
}),
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
}),
}),
});
me.map.addInteraction(me.draw);
me.createMeasureTooltip();
me.createHelpTooltip();
let listener;
me.draw.on('drawstart', function (evt) {
let tooltipCoord = evt.coordinate;
GisApp.measure.sketch = evt.feature;
listener = GisApp.measure.sketch.getGeometry().on('change', function (evt) {
let geom = evt.target;
let output;
if (geom instanceof Polygon) {
output = me.formatArea(geom);
tooltipCoord = geom.getInteriorPoint().getCoordinates();
} else if (geom instanceof LineString) {
output = me.formatLength(geom);
tooltipCoord = geom.getLastCoordinate();
}
//GisApp.measure.measureTooltipElement.innerHTML = DOMPurify.sanitize(output);
GisApp.measure.measureTooltipElement.innerHTML = output;
GisApp.measure.measureTooltip.setPosition(tooltipCoord);
});
});
me.draw.on('drawend', function () {
GisApp.measure.measureTooltipElement.className = 'ol-tooltip ol-tooltip-static';
GisApp.measure.measureTooltip.setOffset([0, -7]);
GisApp.measure.sketch = null;
// unset tooltip so that a new one can be created
GisApp.measure.measureTooltipElement = null;
me.createMeasureTooltip();
unByKey(listener);
me.removeInteraction();
});
},
removeInteraction: function () {
this.map.removeInteraction(this.draw);
this.map.removeOverlay(GisApp.measure.helpTooltip);
},
removeFeature: function () {
let source = this.vector.getSource();
let features = this.vector.getSource().getFeatures();
for (let i in features) {
let feature = features[i];
source.removeFeature(feature);
}
},
createHelpTooltip: function () {
let me = this;
if (GisApp.measure.helpTooltipElement) {
GisApp.measure.helpTooltipElement.parentNode.removeChild(GisApp.measure.helpTooltipElement);
}
GisApp.measure.helpTooltipElement = document.createElement('div');
GisApp.measure.helpTooltipElement.className = 'ol-tooltip hidden';
GisApp.measure.helpTooltip = new Overlay({
element: GisApp.measure.helpTooltipElement,
offset: [15, 0],
positioning: 'center-left',
});
me.map.addOverlay(GisApp.measure.helpTooltip);
},
createMeasureTooltip: function () {
let me = this;
if (GisApp.measure.measureTooltipElement) {
GisApp.measure.measureTooltipElement.parentNode.removeChild(GisApp.measure.measureTooltipElement);
}
GisApp.measure.measureTooltipElement = document.createElement('div');
GisApp.measure.measureTooltipElement.className = 'ol-tooltip ol-tooltip-measure';
GisApp.measure.measureTooltip = new Overlay({
id: 'measure',
element: GisApp.measure.measureTooltipElement,
offset: [0, -15],
positioning: 'bottom-center',
});
me.map.addOverlay(GisApp.measure.measureTooltip);
},
};
export default GisOlMeasure;
@@ -0,0 +1,436 @@
import GisApp from './gis.core';
import _ from 'lodash';
import Style from 'ol/style/Style.js';
import { click, mouseOnly, never } from 'ol/events/condition';
import DragBox from 'ol/interaction/DragBox.js';
import Draw from 'ol/interaction/Draw.js';
import Select from 'ol/interaction/Select.js';
import VectorLayer from 'ol/layer/Vector.js';
import Overlay from 'ol/Overlay.js';
import VectorSource from 'ol/source/Vector.js';
import CircleStyle from 'ol/style/Circle.js';
import Fill from 'ol/style/Fill.js';
import Stroke from 'ol/style/Stroke.js';
//import DOMPurify from 'dompurify';
const GisOlSelect = function (map, options) {
GisApp.select = {};
GisApp.select.helpTooltipElement = null;
GisApp.select.helpTooltip = null;
GisApp.select.helpMsg = null;
this.initialize(map, options);
};
GisOlSelect.prototype = {
map: null,
defaults: {
hitTolerance: 10,
useAreaControl: false,
},
selectInteraction: null,
dragBox: null,
dragPolygon: null,
selectedFeatures: [],
selectFeatureFlag: false,
initialize: function (map, options) {
this.map = map;
_.extend(this.defaults, options);
this.setInteraction();
this.pointerMoveEventHandler();
},
/**
* SELECT 이벤트 등록
*/
setInteraction: function () {
let me = this;
let map = this.map;
if (this.selectInteraction) {
this.removeInteration();
}
let baseStyle = new Style({
image : new CircleStyle({
fill : new Fill({
color : 'rgba(135, 116, 243, 0.4)',
}),
radius : 30
}),
stroke : new Stroke({
color : 'rgba(135, 116, 243, 0.4)',
width : 10,
})
});
this.selectInteraction = new Select({
style: function (feature, resolution) {
let styles = [];
let layer = feature.getLayer();
let styleFn = GisApp.Module.style.getLayerStyleFunction(layer, null);
if (styleFn) {
const style = styleFn(feature, resolution);
if (Array.isArray(style)) {
styles.push(...style);
} else {
styles.push(style);
}
}
styles.push(baseStyle);
return styles;
},
layers: function (layer) {
let clickPsblYn = layer?.get('clickPsblYn');
return clickPsblYn === 'Y';
},
condition: click,
hitTolerance: me.defaults.hitTolerance,
});
map.addInteraction(this.selectInteraction);
this.selectedFeatures = this.selectInteraction.getFeatures();
// 선택된 피처 중심으로 지도 이동
this.selectInteraction.on('select', function (e) {
const feature = e.selected[0];
if (feature) {
const geometry = feature.getGeometry();
if (geometry) {
const extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16
});
}
}
});
},
/**
* SELECT 피처 초기화
*/
reset: function () {
let me = this;
if (GisApp.Module.edit) {
GisApp.Module.edit.setDrawActive(false);
}
if (GisApp.Module.draw) {
GisApp.Module.draw.setDrawActive(false);
}
me.setActive(true);
me.removeHelpTooltip();
let map = this.map;
map.removeInteraction(me.dragBox);
map.removeInteraction(me.dragPolygon);
this.selectInteraction.addCondition_ = never;
this.selectFeatureFlag = false;
if (GisApp.Module.measure) {
GisApp.Module.measure.removeFeature();
GisApp.Module.core.deleteOverlay('measure');
}
GisApp.Module.core.deleteLayer(GisApp.hLayerId);
GisApp.Module.core.deleteLayer(GisApp.hlLayerId);
this.resetSelectedFeature();
},
/**
* 단일 SELECT 이벤트 등록
*/
setSingleInteration: function () {
this.reset();
this.selectInteraction.addCondition_ = click;
this.selectFeatureFlag = true;
},
/**
* 박스 SELECT 이벤트 등록
*/
setBoxInteration: function () {
//초기화
this.reset();
this.selectFeatureFlag = true;
let me = this;
let map = this.map;
me.dragBox = new DragBox({
condition: mouseOnly,
});
map.addInteraction(this.dragBox);
GisApp.select.helpMsg = '직사각형 시작점을 클릭하고 드래그하세요.';
me.createHelpTooltip();
me.dragBox.on('boxend', function () {
let rotation = map.getView().getRotation();
let oblique = rotation % (Math.PI / 2) !== 0;
let candidateFeatures = oblique ? [] : me.selectedFeatures;
let extent = me.dragBox.getGeometry().getExtent();
let layers = map.getLayers();
for (let i in layers.array_) {
let layer = layers.array_[i];
if (layer?.get('clickPsblYn') == 'Y' && me.getVisible(layer)) {
layer.getSource().forEachFeatureIntersectingExtent(extent, function (feature) {
candidateFeatures.push(feature);
});
}
}
if (oblique) {
let anchor = [0, 0];
let geometry = me.dragBox.getGeometry().clone();
geometry.rotate(-rotation, anchor);
let extent$1 = geometry.getExtent();
candidateFeatures.forEach(function (feature) {
let geometry = feature.getGeometry().clone();
geometry.rotate(-rotation, anchor);
if (geometry.intersectsExtent(extent$1)) {
me.selectedFeatures.push(feature);
}
});
}
me.selectInteraction.dispatchEvent({
type: 'select',
selected: me.selectedFeatures,
deselected: [],
});
map.removeInteraction(me.dragBox);
});
me.dragBox.on('boxstart', function () {
me.removeHelpTooltip();
me.selectedFeatures.clear();
});
},
/**
* 폴리곤 SELECT 이벤트 등록
*/
setPolygonInteration: function () {
this.reset();
this.selectFeatureFlag = true;
let me = this;
let map = this.map;
//폴리곤 레이어 생성
let polygonSource = new VectorSource({
useSpatialIndex: false,
});
let polygonLayer = new VectorLayer({
source: polygonSource,
});
map.addLayer(polygonLayer);
//폴리곤 draw 객체 생성
me.dragPolygon = new Draw({
source: polygonSource,
type: 'Polygon',
condition: mouseOnly,
});
map.addInteraction(me.dragPolygon);
//툴팁 추가
GisApp.select.helpMsg = '시작점을 클릭하세요.';
me.createHelpTooltip();
me.dragPolygon.on('drawstart', function () {
me.removeHelpTooltip();
me.selectedFeatures.clear();
});
me.dragPolygon.on('drawend', function (evt) {
setTimeout(function () {
let polygon = evt.feature.getGeometry();
let layers = map.getLayers();
for (let i in layers.array_) {
let layer = layers.array_[i];
if (layer?.get('clickPsblYn') == 'Y' && me.getVisible(layer)) {
let features = layer.getSource().getFeatures();
for (let j = 0; j < features.length; j++) {
if (polygon.intersectsExtent(features[j].getGeometry().getExtent())) {
me.selectedFeatures.push(features[j]);
}
}
}
}
me.selectInteraction.dispatchEvent({
type: 'select',
selected: me.selectedFeatures,
deselected: [],
});
map.removeInteraction(me.dragPolygon);
map.removeLayer(polygonLayer);
}, 300);
});
return me.dragPolygon;
},
/**
* 레이어 추가
*/
addLayer: function (layer) {
if (layer) {
layer.set('clickPsblYn', 'Y');
}
},
/**
* 레이어 삭제
*/
removeLayer: function (layer) {
if (layer) {
layer.set('clickPsblYn', 'N');
}
},
/**
* 선택된 객체 초기화
*/
resetSelectedFeature: function () {
const selectedFeatures = this.selectedFeatures;
selectedFeatures.forEach(feature => {
selectedFeatures.remove(feature);
});
},
/**
* SELECT 이벤트 활성화
*/
setActive: function (active) {
this.selectInteraction.setActive(active);
},
/**
* SELECT 이벤트 삭제
*/
removeInteration: function () {
let map = this.map;
map.removeInteraction(this.selectInteraction);
this.selectInteraction = null;
//포인터 변경 삭제
this.removePointerMoveEventHandler('pointerMoveForSelect');
},
/**
* 피처 위에 마우스 오버시 포인터 변경
*/
pointerMoveEventHandler: function () {
let me = this;
let map = this.map;
let pointerMoveForSelect = function (evt) {
let hit = this.forEachFeatureAtPixel(
evt.pixel,
function (feature, layer) {
let returnFlag = true;
if (layer?.get('clickPsblYn') === 'N') {
returnFlag = false;
}
return returnFlag;
},
{ hitTolerance: me.defaults.hitTolerance }
);
if (hit && me.selectInteraction.getActive()) {
this.getTargetElement().style.cursor = 'pointer';
} else {
this.getTargetElement().style.cursor = '';
}
};
map.on('pointermove', pointerMoveForSelect);
},
/**
* 포인터 이벤트 삭제
*/
removePointerMoveEventHandler: function (fnNm) {
GisApp.Module.core.removeMapEventFnNm('pointermove', fnNm);
},
/**
* 선택된 피처 정보 반환(callback)
*/
getSelectedInfo: function (callback) {
let me = this;
this.selectInteraction.on('select', function (e) {
if (!e.auto) {
let features = me.selectInteraction.getFeatures();
if(features.getArray().length == 0) return;
let layers = [];
for (let i in features.array_) {
let feature = features.array_[0];
let layer = me.selectInteraction.getLayer(feature);
layers.push(layer);
}
callback(features.array_, layers);
}
});
},
/**
* 선택된 피처 정보 반환
*/
getSelectedFeatures: function () {
return this.selectInteraction.getFeatures();
},
/**
* 마지막으로 선택된 피처 정보 반환
*/
getLastSelectedFeatures: function () {
return this.selectInteraction.getFeatures();
},
/**
* 툴팁 생성
*/
createHelpTooltip: function () {
let me = this;
let map = this.map;
GisApp.select.helpTooltipElement = document.createElement('div');
GisApp.select.helpTooltipElement.className = 'ol-tooltip hidden';
GisApp.select.helpTooltip = new Overlay({
element: GisApp.select.helpTooltipElement,
offset: [15, 0],
positioning: 'center-left',
});
map.addOverlay(GisApp.select.helpTooltip);
map.on('pointermove', me.pointerMoveTooltipHandler);
},
/**
* 툴팁 삭제
*/
removeHelpTooltip: function () {
if (GisApp.select.helpTooltipElement) {
let map = this.map;
this.removePointerMoveEventHandler('pointerMoveTooltipHandler');
map.removeOverlay(GisApp.measure.helpTooltip);
GisApp.select.helpTooltipElement.remove();
}
},
/**
* 툴팁용 포인터 move 이벤트
*/
pointerMoveTooltipHandler: function (evt) {
if (evt.dragging) {
return;
}
//GisApp.select.helpTooltipElement.innerHTML = DOMPurify.sanitize(GisApp.select.helpMsg);
GisApp.select.helpTooltipElement.innerHTML = GisApp.select.helpMsg;
GisApp.select.helpTooltip.setPosition(evt.coordinate);
GisApp.select.helpTooltipElement.classList.remove('hidden');
},
getVisible: function (layer) {
let maxZoom = layer.getMaxZoom();
let minZoom = layer.getMinZoom();
let map = this.map;
let curZoom = map.getView().getZoom();
if (layer.getVisible() && maxZoom > curZoom && minZoom < curZoom) {
return true;
}
return false;
},
/**
* 피처 셀렉트
*/
selectFeature: function (feature, auto) {
let me = this;
me.selectedFeatures.clear();
me.selectedFeatures.push(feature);
me.selectInteraction.dispatchEvent({
type: 'select',
selected: me.selectedFeatures,
deselected: [],
auto: auto,
});
},
};
export default GisOlSelect;
@@ -0,0 +1,402 @@
import GisApp from './gis.core.js';
import Style from 'ol/style/Style.js';
import Fill from 'ol/style/Fill.js';
import Icon from 'ol/style/Icon.js';
import Circle from 'ol/style/Circle.js';
import Stroke from 'ol/style/Stroke.js';
import Text from 'ol/style/Text.js';
import Point from 'ol/geom/Point.js';
import RegularShape from 'ol/style/RegularShape.js';
import Overlay from 'ol/Overlay';
import _ from 'lodash';
//import DOMPurify from 'dompurify';
import { createPortal } from 'react-dom';
import ReactDOM from 'react-dom/client';
import React from 'react';
const GisOlStyle = function () {
this.initialize();
};
GisOlStyle.prototype = {
overlays: {},
hiddenOverlays: {},
initialize: function () {
this.layericonPath = GisApp.CONTEXT_LAYER_PATH;
},
getRGBAToString: function (r, g, b, a) {
let rgba = 'rgba({r}, {g}, {b}, {a})';
return rgba.replace('{r}', r).replace('{g}', g).replace('{b}', b).replace('{a}', a);
},
getFillColor: function (layer) {
let r = layer.get('fillColorR');
let g = layer.get('fillColorG');
let b = layer.get('fillColorB');
let a = layer.get('fillColorA');
return this.getRGBAToString(r, g, b, a);
},
getFontLineColor: function (layer) {
let r = layer.get('fontOutlineColorR');
let g = layer.get('fontOutlineColorG');
let b = layer.get('fontOutlineColorB');
let a = layer.get('fontOutlineColorA');
return this.getRGBAToString(r, g, b, a);
},
getFontFillColor: function (layer) {
let r = layer.get('fontColorR');
let g = layer.get('fontColorG');
let b = layer.get('fontColorB');
let a = layer.get('fontColorA');
return this.getRGBAToString(r, g, b, a);
},
point: function (layer, options, feature, resolution) {
let symbol = layer.get('iconNm');
let maticKey = layer.get('maticKey');
let maticVal1 = layer.get('maticVal1');
let image;
let style = new Style({
fill: new Fill(),
});
let text = this.getText(layer, feature, resolution);
style.setText(text);
if (symbol) {
let rotation = 0;
if (feature) {
let angDir = feature.get('angDir');
if (angDir) {
rotation = (angDir * Math.PI) / 180;
}
}
image = new Icon({
crossOrigin: 'anonymous',
scale: 1.2,
src: this.layericonPath + symbol,
rotation: rotation,
});
} else {
image = new Circle({
fill: new Fill({
color: '#4848B4',
}),
radius: 5,
});
}
const overlayHtml = feature.get('html');
if (overlayHtml) {
this.setOverlay(layer, feature, overlayHtml);
}
let baseResolution = 20;
let minScale = 0.3;
let maxScale = 1.5;
let scale = baseResolution / resolution > maxScale ? maxScale : baseResolution / resolution;
scale = Math.max(minScale, Math.min(maxScale, scale));
image.setScale(scale);
style.setImage(image);
_.extend(style, options);
return style;
},
line: function (layer, options, feature, resolution) {
let linewidth = layer.get('lnWdth');
let lineColor = layer.get('lnColor');
let style = new Style({
stroke: new Stroke({
color: lineColor,
width: linewidth,
}),
text: new Text(),
});
if (GisApp.Module.toolbar && GisApp.Module.toolbar.defaults.blockDiagramVisible) {
let blockDiagramLayerId = GisApp.Module.toolbar.getBlockDiagramLayerId(layer.get('lyrId'));
if (blockDiagramLayerId) {
style = this.getArrowStyle(feature, lineColor, linewidth);
}
}
return style;
},
poligon: function (layer, options, feature, resolution) {
const style = new Style();
const color = layer.get('polgnColor');
const fill = new Fill({
color,
});
style.setFill(fill);
_.extend(style, options);
return style;
},
lineLabelPoligon: function (layer, options, feature, resolution) {
let style = new Style();
const polgnColor = layer.get('polgnColor');
let fill = new Fill({
color: polgnColor,
});
let lnWdth = layer.get('lnWdth');
let lineColor = layer.get('lnColor');
let lineDash = layer.get('lineDash');
let stroke = new Stroke({
color: lineColor || '#000000',
width: lnWdth,
lineDash: lineDash ? [10, 5] : null,
});
let text = this.getText(layer, feature, resolution);
if (text) {
style.setText(text);
}
const overlayHtml = feature.get('html');
if (overlayHtml) {
this.setOverlay(layer, feature, overlayHtml);
}
style.setFill(fill);
style.setStroke(stroke);
_.extend(style, options);
return style;
},
imagePoligon: function (layer, options, feature, resolution) {
let style = new Style();
let symbol = layer.get('iconNm');
let retGeom = feature.getGeometry().getInteriorPoint();
let image = new Icon({
crossOrigin: 'anonymous',
imgSize: [20, 20],
src: this.layericonPath + symbol,
});
let scale = 0.5 / resolution > 1 ? 1 : 0.5 / resolution;
image.setScale(1);
style.setGeometry(retGeom);
style.setImage(image);
_.extend(style, options);
return style;
},
setOverlay: function (layer, feature, overlayHtml) {
const layerFeature = layer.getSource().getFeatures()[0];
const layerCoordinate = layerFeature ? layerFeature.getGeometry().getCoordinates() : null;
const properties = feature.getProperties();
const overLayId = feature?.get('overLayId');
const overLayMinZoom = layer.get('overLayMinZoom') || '12';
const overLayOffset = layer?.get('overLayOffset');
let findoOverlay = this.overlays[overLayId];
if (findoOverlay) {
const zoom = GisApp.Module.core.map.getView().getZoom();
if (Number(overLayMinZoom) < zoom) {
findoOverlay.options.element.style.display = 'block';
} else {
findoOverlay.options.element.style.display = 'none';
}
return findoOverlay;
}
let overlayContainer = document.createElement('div');
document.body.appendChild(overlayContainer);
if (typeof overlayHtml === 'string') {
//const sanitizedHtml = DOMPurify.sanitize(overlayHtml);
overlayContainer.innerHTML = sanitizedHtml;
}
// React 컴포넌트일 경우 createPortal() 사용
else if (React.isValidElement(overlayHtml)) {
ReactDOM.createRoot(overlayContainer).render(createPortal(overlayHtml, overlayContainer));
}
// 오버레이 생성
let overlay = new Overlay({
element: overlayContainer,
positioning: 'bottom-center',
stopEvent: false,
offset: overLayOffset ? overLayOffset : [0, 0], // 오프셋 설정 (위치 조정)
});
this.overlays[overLayId] = overlay;
// feature의 좌표 가져오기
let geometry = feature.getGeometry();
let coordinate;
// Point인 경우
if (geometry.getType() === 'Point') {
coordinate = geometry.getCoordinates();
}
// LineString 또는 MultiLineString인 경우
else if (geometry.getType() === 'LineString' || geometry.getType() === 'MultiLineString') {
coordinate = geometry.getFlatMidpoint(); // 선의 중간 좌표를 가져옴
}
// Polygon 또는 MultiPolygon인 경우
else if (geometry.getType() === 'Polygon' || geometry.getType() === 'MultiPolygon') {
coordinate = geometry.getExtent();
const center = [(coordinate[0] + coordinate[2]) / 2, (coordinate[1] + coordinate[3]) / 2];
coordinate = center;
} else {
console.error('Unsupported geometry type: ' + geometry.getType());
return;
}
overlay.setPosition(coordinate);
GisApp.Module.core.map.addOverlay(overlay);
const zoom = GisApp.Module.core.map.getView().getZoom();
if (Number(overLayMinZoom) < zoom) {
overlay.options.element.style.display = 'block';
} else {
overlay.options.element.style.display = 'none';
}
},
label: function (layer, options, feature, resolution) {
let textBaseline = layer.get('textBaseline') || 'bottom';
let textAlign = layer.get('textAlign') || 'center';
let fontExtnColor = layer.get('fontExtnColor');
let fontExtnWdth = Number(layer.get('fontExtnWdth') || '3.5');
let fontIntnColor = layer.get('fontIntnColor');
let style = new Style();
let prop = feature.getProperties();
let label = prop[layer.get('lblAtrbNm')];
let text = new Text({
stroke: new Stroke({
width: fontExtnWdth,
color: fontExtnColor,
}),
fill: new Fill({
color: fontIntnColor,
}),
font: layer.get('font'),
textBaseline: textBaseline,
textAlign: textAlign,
text: label,
});
style.setStroke(null);
style.setFill(null);
style.setText(text);
_.extend(style, options);
return style;
},
getText: function (layer, feature, resolution) {
let textBaseline = layer.get('textBaseline') || 'middle';
let textAlign = layer.get('textAlign') || 'center';
let fontExtnColor = layer.get('fontExtnColor');
let fontExtnWdth = Number(layer.get('fontExtnWdth') || '1');
let fontIntnColor = layer.get('fontIntnColor');
let minViewSize = layer.get('minViewSize');
if (feature == null) {
return;
}
let prop = feature.getProperties();
let label = prop[layer.get('lblAtrbNm')];
let scaleFactor = 1 / resolution; // 해상도가 낮아질수록 글자 크기 증가
const zoom = GisApp.Module.core.map.getView().getZoom();
if (zoom < minViewSize - 3) {
label = '';
}
let text = new Text({
stroke: new Stroke({
color: '#FFFFFF', // 흰색 테두리
width: fontExtnWdth,
}),
fill: new Fill({
color: fontIntnColor,
width: fontExtnWdth,
}),
font: `${layer.get('fontNm')}`, // 동적으로 폰트 크기 적용
textBaseline: textBaseline,
textAlign: textAlign,
overflow: true,
text: label,
offsetY: 20,
});
return text;
},
getArrowStyle: function (feature, lineColor, linewidth) {
let styles = [
new Style({
stroke: new Stroke({
color: lineColor,
width: linewidth,
}),
}),
];
let geomLienString = GisApp.Module.core.getGeomLineOfFeature(feature);
if (geomLienString) {
let startCoord = geomLienString.getFirstCoordinate();
let endCoord = geomLienString.getLastCoordinate();
let dx = endCoord[0] - startCoord[0];
let dy = endCoord[1] - startCoord[1];
let rotation = Math.atan2(dy, dx);
let centerCoord = GisApp.Module.core.getCenterOfLineFeature(feature);
styles.push(
new Style({
geometry: new Point(centerCoord),
image: new RegularShape({
fill: new Fill({ color: lineColor }),
points: 3,
radius: 6,
rotation: -rotation,
angle: Math.PI / 2,
}),
})
);
}
return styles;
},
//레이어 스타일 반환
getLayerStyle: function (layer, options) {
let gobjTy = layer.get('gobjTy');
if (!gobjTy) return;
let style = this[gobjTy](layer, options);
return style;
},
getLayerStyleFunction: function (layer, options) {
let gobjTy = layer.get('gobjTy');
let styleFunction = function (feature, resolution) {
return this[gobjTy](layer, options, feature, resolution);
}.bind(this);
return styleFunction;
},
adjustOverlayPosition: function (overlay, coordinate) {
const overlays = this.overlays;
let adjustedCoordinate = [...coordinate]; // 초기 좌표 설정
const minDistance = 100000; // 최소 거리 (필요시 조정)
let offset = overlay.getOffset() || [0, 0];
let closestOverlay = null;
let closestDistance = Infinity;
// 모든 오버레이의 위치를 비교하여 가장 가까운 오버레이 찾기
Object.values(overlays).forEach(existingOverlay => {
if (existingOverlay) {
const existingCoordinate = existingOverlay.getPosition();
const existingOffset = existingOverlay.getOffset() || [0, 0];
// 현재 오버레이의 좌표 가져오기
const overlayCoordinate = overlay.getPosition();
if (!overlayCoordinate || !existingCoordinate) return;
// 거리 계산 (유클리드 거리)
const distance = Math.sqrt(Math.pow(overlayCoordinate[0] - existingCoordinate[0], 2) + Math.pow(overlayCoordinate[1] - existingCoordinate[1], 2));
// 가장 가까운 오버레이 업데이트
if (distance > 0 && distance < closestDistance) {
closestDistance = distance;
closestOverlay = existingOverlay;
} else {
offset = [50, 50];
}
}
});
// 가장 가까운 오버레이가 있으면 offset 조정
if (closestOverlay && closestDistance < minDistance) {
const closestOffset = closestOverlay.getOffset() || [0, 0];
// X, Y 좌표를 기존 offset 값의 반대로 설정
offset = [-closestOffset[0], -closestOffset[1]];
}
// 최종 offset 적용
overlay.setOffset(offset);
},
};
export default GisOlStyle;
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,596 @@
//공통
export const util = {
getOneToArrayByObjectKey: function (arr, key, value) {
let result = {};
arr.forEach(item => {
if (item[key] === value) {
result = item;
}
});
return result;
},
groupByProperty: (arr, prop, parentProp, group) => {
const tree = [];
const mappedArr = {};
let arrElem;
let mappedElem;
// 먼저 배열의 노드를 개체에 매핑하고 -> 해시 테이블을 생성
for (let i = 0; i < arr.length; i++) {
arrElem = arr[i];
if (group && arrElem.mnuGroup !== group) continue;
mappedArr[arrElem[prop]] = arrElem;
mappedArr[arrElem[prop]].children = [];
}
for (const mnuCd in mappedArr) {
if (mappedArr.hasOwnProperty(mnuCd)) {
mappedElem = mappedArr[mnuCd];
// 요소가 루트 수준에 없으면 자식의 부모 배열에 추가.
if (mappedElem[parentProp] && mappedArr[mappedElem[parentProp]]?.children) {
mappedArr[mappedElem[parentProp]].children.push(mappedElem);
}
// 요소가 루트 수준에 있는 경우 첫 번째 수준 요소 배열에 추가
else {
tree.push(mappedElem);
}
}
}
return tree;
},
getLowestChildren: function (obj) {
if (!obj.children || obj.children.length === 0) {
return obj;
} else {
return this.getLowestChildren(obj.children[0]);
}
},
hasClass: (e, clazz) => {
return e.target.classList.contains(clazz);
},
isNull: function (str) {
if (typeof str === 'undefined' || str === null || str === 'null' || str === '' || str === 'undefined') {
return true;
}
return false;
},
nvl: function (str, returnVal) {
if (this.isNull(str)) {
return returnVal;
}
return str;
},
isNumeric: function (str) {
const pattern = /\d/;
return pattern.test(str);
},
isAlpha: function (str) {
const pattern = /[a-zA-Z]/;
return pattern.test(str);
},
isAlphaNumeric: function (str) {
const pattern = /^[a-zA-Z0-9]+$/;
return pattern.test(str);
},
isSpecialChar: function (str) {
const pattern = /[~!@#$%^&*()_+|<>?:{}]/;
return pattern.test(str);
},
isKoreanAplhaNumeric: function (str) {
const pattern = /^[ㄱ-ㅎ|ㅏ-ㅣ|가-힣|a-zA-Z0-9]+$/;
return pattern.test(str);
},
isValidPassword: function (str) {
const pattern = /^(?=.*[a-zA-Z])(?=.*[0-9])(?=.*[!@#$%^&*(),.?":{}|<>])[a-zA-Z0-9!@#$%^&*(),.?":{}|<>]{8,20}$/;
return pattern.test(str);
},
isValidExt: function (fileName, allowedExtensions) {
const fileExtension = fileName.split('.').pop().toLowerCase();
return allowedExtensions.includes(fileExtension);
},
isValidAllDate: function (dateString) {
const formats = ['YYYY', 'YYYYMM', 'YYYYMMDD'];
for (let format of formats) {
if (moment(dateString, format, true).isValid()) {
return true;
}
}
return false;
},
isEmptyObject: function (param) {
return Object.keys(param).length === 0 && param.constructor === Object;
},
castArrayObjectToList: function (array, key) {
var newArray = [];
for (var i = 0; i < array.length; i++) {
newArray.push(array[i][key]);
}
return newArray;
},
arraysMatch: function (arr1, arr2) {
// 길이가 같은지 확인
if (arr1.length !== arr2.length) return false;
// 각 요소가 일치하는지 확인
return arr1.every((element, index) => element === arr2[index]);
},
//상위뎁스 객체들 조회
findObjectByKeyAndValue: function ({ array, find: { key: findKey, value: findValue }, result: { key: resultKey }, parents = [] }) {
for (let i = 0; i < array.length; i++) {
let obj = array[i];
let siblings = [...array];
//siblings.splice(i, 1); // 현재 메뉴를 제거하여 형제 메뉴들만 남김
let currentParents = [...parents, { [resultKey]: obj, siblings: siblings }];
if (obj[findKey] === findValue) {
let formattedResult = currentParents.reduce((acc, cur, idx) => {
acc[`${resultKey}${idx + 1}`] = { active: cur[resultKey], list: cur.siblings };
return acc;
}, {});
return formattedResult;
}
if (obj.children) {
let result = this.findObjectByKeyAndValue({ array: obj.children, find: { key: findKey, value: findValue }, result: { key: resultKey }, parents: currentParents });
if (result !== null) {
return result;
}
}
}
return null;
},
autoComma(val, returnVal) {
if (!val && val !== 0) return returnVal ? returnVal : '';
// 문자열로 변환하고, 숫자와 소수점만을 허용
val = String(val).replace(/[^0-9.-]/g, '');
// 소수점을 기준으로 숫자를 분리
let [integerPart, decimalPart] = val.split('.');
// 정수 부분에 대해 쉼표 추가
integerPart = integerPart.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
// 소수점 이하 숫자가 있다면 다시 합치기
return decimalPart ? `${integerPart}.${decimalPart}` : integerPart;
},
removeComma: function (str) {
str = String(str);
return str.replace(/[^\d]+/g, '');
},
removeSpaceAndTrim: function (str) {
if (str) {
str = String(str);
return str.replace(/\s/g, '');
} else {
return str;
}
},
isNotWhiteSpace: function (str) {
const hasWhitespace = /\s/.test(str);
return !hasWhitespace;
},
//숫자만 남기고 모두 제거해서 반환
retainOnlyNumbers: function (str) {
str = String(str);
return str.replace(/\D+/g, '');
},
proxyToObject: function (proxy) {
return JSON.parse(JSON.stringify(Object.assign({}, proxy)));
},
dateFormatter: function ({ input, isHan = false }) {
const value = this.retainOnlyNumbers(input);
if (value.length === 6) {
const date = moment(value, 'YYYYMM');
return date.format(isHan ? 'YYYY년 MM월' : 'YYYY-MM');
} else if (value.length === 8) {
const date = moment(value, 'YYYYMMDD');
return date.format(isHan ? 'YYYY년 MM월 DD일' : 'YYYY-MM-DD');
} else if (value.length === 14) {
const date = moment(value, 'YYYYMMDDHHmmss');
return date.format(isHan ? 'YYYY년 MM월 DD일 HH시 mm분 ss초' : 'YYYY-MM-DD HH:MM:SS');
} else {
return input;
}
},
generateYearArray: function (num) {
const currentYear = moment().year();
if (num <= 1 || isNaN(num)) {
return [(currentYear - 2).toString(), (currentYear - 1).toString(), currentYear.toString()];
}
const years = [];
for (let i = num - 1; i >= 0; i--) {
years.push((currentYear - i).toString());
}
return years;
},
getFileId: function () {
let timeStamp = moment().format('YYYYMMDDHHmmssSSS'); // YYYYMMDDHHmmssSSS 형태로 타임스탬프를 생성
return 'CFM_' + timeStamp.slice(-12); // slice(-12)는 문자열의 끝에서 12자리를 추출합니다.
},
lpad: function (number, pad, length) {
let str = number.toString();
while (str.length < length) {
str = pad + str;
}
return str;
},
convertKeysToCamelCase: function (obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
return Object.keys(obj).reduce((acc, key) => {
const camelCaseKey = key.replace(/([-_]\w)/g, match => match.replace(/[-_]/, '').toUpperCase());
acc[camelCaseKey] = typeof obj[key] === 'object' ? this.convertKeysToCamelCase(obj[key]) : obj[key];
return acc;
}, {});
},
blobUrlToBlob: async blobUrl => {
try {
const response = await fetch(blobUrl);
return await response.blob();
} catch (error) {
console.error(error);
return new Blob();
}
},
hexToRgba: hex => {
const r = parseInt(hex.substring(1, 3), 16);
const g = parseInt(hex.substring(3, 5), 16);
const b = parseInt(hex.substring(5, 7), 16);
return `rgba(${r}, ${g}, ${b} , 0.5)`;
},
rgbaToHex: rgba => {
/*rgba값을 가져와서 rgb 로 추출 후 hex 값 반환 */
if (!rgba) return false;
const rgbArray = rgba.match(/\d+/g).slice(0, 3).map(Number);
return `#${((1 << 24) | (rgbArray[0] << 16) | (rgbArray[1] << 8) | rgbArray[2]).toString(16).slice(1).toUpperCase()}`;
},
rgbAddOpacity: (rgb, opacity) => {
const rgbValues = rgb.match(/\d+/g); // rgb 값을 배열로 추출
const colorWithOpacity = `rgba(${rgbValues[0]}, ${rgbValues[1]}, ${rgbValues[2]}, ${opacity})`; // rgba로 변환
return colorWithOpacity;
},
generateMonthArray: (pSmonth, pEmonth) => {
let startYear = Math.floor(pSmonth / 100);
let startMonth = pSmonth % 100;
let endYear = Math.floor(pEmonth / 100);
let endMonth = pEmonth % 100;
let months = [];
for (let year = startYear; year <= endYear; year++) {
let monthStart = year === startYear ? startMonth : 1;
let monthEnd = year === endYear ? endMonth : 12;
for (let month = monthStart; month <= monthEnd; month++) {
let monthString = month < 10 ? `0${month}` : `${month}`;
months.push(`${year}${monthString}`);
}
}
return months;
},
findDuplicatesForArr: (arr, key) => {
const map = new Map();
const duplicates = [];
arr.forEach(item => {
const keyValue = item[key];
if (map.has(keyValue)) {
if (!duplicates.includes(keyValue)) {
duplicates.push(keyValue);
}
} else {
map.set(keyValue, true);
}
});
return duplicates;
},
createHtmlFromJson: (html, data) => {
if (!html || util.isEmptyObject(html)) return null;
const { classNm, headerKey, rows } = JSON.parse(html);
if (!rows) return null;
const tableRows = rows
.map(row => {
const value = data?.[row.key] ?? null;
let displayValue;
if (typeof value === 'number') {
displayValue = util.autoComma(value, 0);
} else if (typeof value === 'string') {
displayValue = util.nvl(value, '-');
} else {
displayValue = '-';
}
return `
<tr>
<th>${row.th}</th>
<td>${displayValue}</td>
</tr>
`;
})
.join('');
return `
<div class="${classNm}">
<table>
<tr>
<th colspan="2" style="position: relative; text-align: center; background-color: #DEEAF3;">
${data?.[headerKey] ?? '-'}
</th>
</tr>
${tableRows}
</table>
</div>
`;
},
};
/**
*
* 다이렉트로 import하여 사용하기 위해 export로 수정중
*
* */
//객체 배열에서 특정 key의 value인 것을 추출
export const getArrayByObjectKey = function (arr, key, value) {
return arr.filter(item => item[key] === value);
};
//소수점 1자리를 기준으로 점을 언더스코어로 변경
export const numberDotToUnderscore = function (num) {
if (Number.isInteger(num)) {
return num.toString();
} else {
return num.toFixed(1).replace('.', '_');
}
};
export const stripHTML = function (htmlString) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlString, 'text/html');
return doc.body.textContent || '';
};
//URL에서 매개변수 추출
export function getParameterByName(name, url = window.location.search) {
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
export function dateFormatForDB({ date: isoString, format = 'YYYY-MM-DD HH:MI:SS' }) {
if (!isoString) return '';
let date = new Date(isoString);
let year = date.getFullYear();
let month = (date.getMonth() + 1).toString().padStart(2, '0');
let day = date.getDate().toString().padStart(2, '0');
let hours = date.getHours().toString().padStart(2, '0');
let minutes = date.getMinutes().toString().padStart(2, '0');
let seconds = date.getSeconds().toString().padStart(2, '0');
return (
format
//
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('MI', minutes)
.replace('SS', seconds)
);
}
//blob 다운로드
//blob 다운로드
export function downloadFromBlob(res, data) {
const url = URL.createObjectURL(new Blob([res.data], { type: res.data.type }));
const link = document.createElement('a');
const contentDisposition = res.headers?.['content-disposition']; // 파일 이름
let fileName = data?.fileName || 'export'; //기본 파일 이름
if (contentDisposition) {
const [fileNameMatch] = contentDisposition.split(';').filter(str => str.includes('filename'));
if (fileNameMatch) {
[, fileName] = fileNameMatch.split('=');
// 파일명이 "UTF-8''" 등으로 인코딩되어 있을 수 있으니 decodeURIComponent 추가
fileName = decodeURIComponent(fileName);
}
}
link.href = url;
// 브라우저 인코딩 관련 이슈 방지를 위해 파일명에 대한 추가 decodeURI 처리
link.download = decodeURIComponent(fileName).replace(/"/g, '').trim();
link.style.display = 'none';
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
}
export function downloadBlobAsFile(blob, filename = 'export') {
// 브라우저에 따라 인코딩 처리 햔재는 파이어폭스만
const url = URL.createObjectURL(blob);
const encodedFilename = encodeURIComponent(filename);
const downloadLink = document.createElement('a');
downloadLink.href = url;
downloadLink.download = decodeURIComponent(encodedFilename);
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
URL.revokeObjectURL(url);
}
export function viewFile(res, data) {
// Blob URL 생성
const url = URL.createObjectURL(new Blob([res.data], { type: res.data.type }));
let fileType = null;
let fileName = data?.fileName || 'export'; // 기본 파일 이름
// PDF 및 이미지 타입의 MIME 타입 확인
if (res.data.type === 'application/pdf') {
fileType = 'pdf';
} else if (res.data.type.startsWith('image/')) {
fileType = 'img';
} else if (res.data.type === 'application/octet-stream') {
// Content-Disposition 헤더에서 파일 이름 추출
const contentDisposition = res.headers?.['content-disposition'];
if (contentDisposition) {
const fileNameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (fileNameMatch && fileNameMatch[1]) {
fileName = fileNameMatch[1].replace(/['"]/g, ''); // 파일 이름 추출
fileName = decodeURIComponent(fileName); // URL 인코딩된 파일 이름을 디코딩
}
}
// 파일 확장자 추출
const fileExtension = fileName.split('.').pop().toLowerCase();
// 확장자를 통해 파일 타입 결정
if (fileExtension === 'pdf') {
fileType = 'pdf';
} else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(fileExtension)) {
fileType = 'img';
} else {
console.error('지원하지 않는 파일 형식입니다.');
}
}
// URL과 파일 타입 반환
return { url, fileType, fileName };
}
export function getToday() {
return getDate('today');
}
export function getDate(type, diff) {
const date = new Date();
switch (type) {
case 'year':
date.setFullYear(date.getFullYear() + diff);
break;
case 'month':
date.setMonth(date.getMonth() + diff);
break;
case 'day':
date.setDate(date.getDate() + diff);
break;
case 'today':
date.setDate(date.getDate());
break;
default:
console.log('잘못된 타입입니다. 올바른 타입: 년, 월, 일');
return;
}
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
return `${year}${month}${day}`;
}
//promise 처리
export const byPromise = function (actions) {
return actions.reduce((promise, action) => {
return promise.then(async () => {
if (typeof action === 'function') {
return await action();
} else {
return await action;
}
});
}, Promise.resolve());
};
export const getMessage = (msgGroup, msgKey, innerText, fix) => {
let resultString = GLOBAL.MESSAGE[msgGroup][msgKey];
const prefix = fix?.prefix || '';
const suffix = fix?.suffix || '';
if (innerText) {
for (const [key, value] of Object.entries(innerText)) {
const regex = new RegExp(`\\$\{${key}\}`, 'g');
resultString = resultString.replace(regex, value);
}
}
if (resultString) resultString = resultString.replace(/\\n/g, '\n');
return resultString ? `${prefix}${resultString}${suffix}` : '안내 메시지 로딩 오류 - 관리자에게 문의하세요.';
};
// global.js의 message를 읽어오는 alert
export const gAlert = (msgGroup, msgKey, innerText, fix) => {
alert(getMessage(msgGroup, msgKey, innerText, fix));
};
// message를 confirm
export const gConfirm = (msgGroup, msgKey, innerText, fix) => {
return window.confirm(getMessage(msgGroup, msgKey, innerText, fix));
};
// message를 함수 리턴
export const gMessage = getMessage;
// message를 save후 응답까지 처리
export const gSaveCommonConfirm = async ({ message, api = async () => {}, param = {}, success, fail, prefix }) => {
try {
if (!gConfirm('CONFIRM', `${message}_QUEST`, null, { prefix })) return;
const result = await api(param, { isAlert: false });
if (result?.code !== 200) return;
const rep = message === 'UPLOAD' ? result.data.data : result.data;
if (rep > 0) {
gAlert('INFO', `${message}_COMPLETE`);
await success?.();
} else {
gAlert('ERROR', `${message}_FAIL`);
await fail?.();
}
} catch (error) {
console.error('오류 발생:', error); // 시큐어코딩 관련 수정 : 오류 정보를 로그로 출력
}
};
export const nullToValue = (text, { value = '-', prefix = '', suffix = '' } = {}) => {
if (!text || text === value) {
return value;
}
return `${prefix}${text}${suffix}`;
};
export const filterArrayOfObjectsByKeys = (arrayOfObjects, keys) => {
return arrayOfObjects.map(obj => {
return keys.reduce((filteredObj, key) => {
if (obj.hasOwnProperty(key)) {
filteredObj[key] = obj[key];
}
return filteredObj;
}, {});
});
};
export const isMobile = () => {
const width = window.innerWidth > 0 ? window.innerWidth : window.screen.width;
return width < GLOBAL.MOBILE_SIZE;
};
@@ -5,5 +5,5 @@ function SearchDiv ({children}:{ children: React.ReactNode; }) {
</div>
)
}
export { SearchDiv };
@@ -0,0 +1,18 @@
export default function SearchIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6 text-gray-700"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-4.35-4.35M17 11A6 6 0 105 11a6 6 0 0012 0z"
/>
</svg>
);
}
@@ -0,0 +1,111 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components";
import { useEffect, useState } from "react";
import UiPageNations from "./uiPageNations";
type UiTableProps = {
headName: React.ReactNode | undefined;
tableData: { [key: string]: string | number | boolean | object }[];
headlist: UiHeadProps[];
pageSize: number;
total: number;
children: React.ReactNode | undefined;
cellClick: ((index: number | undefined, headId: string) => void) | undefined;
onPageChange: (page: number) => void;
className?: string;
resetPageNo?: boolean;
cur?: number;
}
type UiHeadProps = {
id: string;
title: string;
align?: string;
}
export function UiTable2({ tableData, headlist, pageSize, total, children, headName, cellClick, onPageChange, className = '', resetPageNo = false, cur }: UiTableProps) {
const [idx, setIdx] = useState<number>(-1);
const [curState, setCurState] = useState<number>(cur ?? 1);
useEffect(() => {
setCurState(cur ?? 1);
}, [cur]);
useEffect(() => {
if (resetPageNo) {
setCurState(1);
onPageChange(1);
}
}, [onPageChange, resetPageNo]);
const handlePageChange = (page: number) => {
setCurState(page);
onPageChange(page);
}
const handleCellClick = (index: number, headId: string) => {
if (cellClick) {
cellClick(index, headId);
setIdx(index);
}
};
const startIdx = (curState - 1) * pageSize;
const endIdx = startIdx + pageSize;
const pageData = tableData.slice(startIdx, endIdx);
return (
<>
{headName ? (
<div className="card-header">
<h3 className="text-2xl font-medium text-label">{headName}</h3>
</div>
) : null}
<div className={`card !m-[1.2rem] !p-0r w-full relative ${className}`}>
<div>
<Table variant="secondary" className="w-full">
{/* 스크롤 관련 div 제거 */}
<TableHeader className="bg-white">
{children ? (
<>{children}</>
) : (
<TableRow>
{headlist?.map((head) => (
<TableHead key={head.id}
className="min-w-[120px] max-w-[180px] w-[150px]
text-center whitespace-nowrap overflow-hidden
text-ellipsis bg-white">
{head.title}
</TableHead>
))}
</TableRow>
)}
</TableHeader>
<TableBody className='text-center'>
{pageData.map((el: { [key: string]: string | number | boolean | object }, index) => (
<TableRow key={index + "row"}>
{headlist?.map((head) => (
<TableCell
className={`${head.align === 'right' ? 'text-right' : head.align === 'center' ? 'text-center' : typeof el[head.id] === 'number' ? 'text-right' : 'text-center'} ${cellClick && idx === index ? '!font-[1000] cursor-pointer' :
cellClick ? 'cursor-pointer' : ''
}`}
onClick={() => handleCellClick(startIdx + index, head.id)}
key={head.id + index}
>
{typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* 페이지네이션을 sticky 하단에 고정 */}
{total > 0 && (
<div className="sticky left-0 right-0 bottom-0 bg-white z-20 border-t border-gray-200">
<UiPageNations total={total} cur={curState} pageSize={pageSize} onPageChange={handlePageChange} />
</div>
)}
</div>
</>
);
}
@@ -30,5 +30,5 @@
"**/*.mjs",
"**/*.jsx",
"**/*.json"
]
, "../../../apps/sn/app/map/SearchModal.tsx" ]
}