gis 오버레이 추가

This commit is contained in:
leedj312
2025-09-02 17:45:36 +09:00
parent d172b17880
commit 44c687d432
3 changed files with 265 additions and 1 deletions
@@ -23,4 +23,5 @@ export * from "./title";
export * from "./sdmInsertForm";
export * from "./popover_ui";
export * from "./date_month_picker";
export * from "./date_picker_range"
export * from "./date_picker_range"
export * from "./map/GisOverlay";
@@ -0,0 +1,216 @@
// Overlay manager (no React here)
import Overlay from 'ol/Overlay';
import { getFeatureLabelCoordinate } from '../map/lib/util';
import { buildCoordinateKeyFactory } from '../map/lib/util';
// ---- main factory ----
export default function createOverlayManager(map, vectorLayer, overlayFormatter, options = {}) {
const coordPrecision = Math.max(0, Math.min(12, Number.isFinite(options.coordPrecision) ? options.coordPrecision : 6));
const maxOverlaysPerViewport = Number.isFinite(options.maxOverlaysPerViewport) ? options.maxOverlaysPerViewport : Infinity;
const getCoordinateKey = buildCoordinateKeyFactory(coordPrecision);
let currentFormatter = overlayFormatter;
const overlayMap = new Map(); // coordKey -> Overlay
const overlayElementMap = new Map(); // coordKey -> HTMLElement
const vectorSource = vectorLayer?.getSource?.();
const SYNC_MIN_INTERVAL = 120;
let lastSyncAt = 0;
let isMounted = false;
let isActive = false; // when false, event handlers no-op
let rafId = null;
let idleClearId = null;
const clearAllOverlaysAsync = (batch = 150) => {
const keys = Array.from(overlayMap.keys());
if (!keys.length) return;
// 재귀로 사용하는 이유 한번에 다지운는ㄴ것이아닌 150개 이상 더있을경우를 대비해서 다시 호출하기 위해(지도가 계속 멈춤)
const step = () => {
let iNum = 0;
const end = Math.min(iNum + batch, keys.length);
for (; iNum < end; iNum++) {
const k = keys[iNum];
const ov = overlayMap.get(k);
try { if (ov) map.removeOverlay(ov); } catch {}
overlayMap.delete(k);
const el = overlayElementMap.get(k);
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); }
}
if (iNum < keys.length) {
(typeof requestAnimationFrame === 'function' ? requestAnimationFrame : setTimeout)(step, 0);
}
};
step();
};
const scheduleSync = () => {
if (!isActive) return;
const now = Date.now();
const since = now - lastSyncAt;
const run = () => {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
try { syncOverlays(); } finally { lastSyncAt = Date.now(); }
});
};
if (since >= SYNC_MIN_INTERVAL) run(); else run();
};
const onVisibleChange = () => { if (!isActive) return; scheduleSync(); };
const onAddFeature = () => { if (!isActive) return; scheduleSync(); };
const onChangeFeature = () => { if (!isActive) return; scheduleSync(); };
const onRemoveFeature = () => { if (!isActive) return; scheduleSync(); };
const clearAllOverlays = () => {
overlayMap.forEach((ov) => { try { map.removeOverlay(ov); } catch {} });
overlayMap.clear();
overlayElementMap.clear();
};
const ensureOverlay = (feature, properties) => {
const coord = getFeatureLabelCoordinate(feature);
if (!Array.isArray(coord)) return null;
const key = getCoordinateKey(coord);
let el = overlayElementMap.get(key);
if (!el) {
el = currentFormatter(feature,properties);
overlayElementMap.set(key, el);
}
let isOverlay = overlayMap.get(key);
if (!isOverlay) {
isOverlay = new Overlay({
element: el,
positioning: 'bottom-center',
offset: [0, -18],
stopEvent: true,
});
overlayMap.set(key, isOverlay);
map.addOverlay(isOverlay);
}
isOverlay.setPosition(coord);
if (el && el.style) el.style.display = 'block';
return isOverlay;
};
const syncOverlays = () => {
if (!vectorLayer?.getVisible?.()) { clearAllOverlays(); return; }
const view = map.getView?.();
const size = map.getSize?.();
const viewExtent = view && size ? view.calculateExtent(size) : null;
// gather features in viewport only and group by coordinate
const groups = new Map(); // coordKey -> feature[]
const addFeature = (f) => {
const c = getFeatureLabelCoordinate(f);
if (!Array.isArray(c)) return;
const k = getCoordinateKey(c);
const arr = groups.get(k);
if (arr) arr.push(f); else groups.set(k, [f]);
};
if (vectorSource?.forEachFeatureInExtent && viewExtent) {
vectorSource.forEachFeatureInExtent(viewExtent, addFeature);
} else {
vectorSource?.forEachFeature(addFeature);
}
// cap overlays per viewport with simple uniform sampling
const keys = Array.from(groups.keys());
let selectedKeys;
if (!isFinite(maxOverlaysPerViewport) || keys.length <= maxOverlaysPerViewport) {
selectedKeys = keys;
} else {
const step = Math.ceil(keys.length / maxOverlaysPerViewport);
selectedKeys = [];
for (let i = 0; i < keys.length && selectedKeys.length < maxOverlaysPerViewport; i += step) {
selectedKeys.push(keys[i]);
}
}
// ensure selected overlays
for (const k of selectedKeys) {
const featuresAtKey = groups.get(k);
const rep = featuresAtKey[0];
const props = rep.getProperties?.() ?? {};
if (featuresAtKey.length > 1) props.__groupCount = featuresAtKey.length;
ensureOverlay(rep, props);
}
// remove non-selected overlays
const keep = new Set(selectedKeys);
for (const [k, ov] of overlayMap.entries()) {
if (!keep.has(k)) {
try { map.removeOverlay(ov); } catch {}
overlayMap.delete(k);
const el = overlayElementMap.get(k);
if (el) { try { el.remove?.(); } catch {} overlayElementMap.delete(k); }
}
}
};
const mount = () => {
if (isMounted) return;
vectorLayer?.on?.('change:visible', onVisibleChange);
vectorSource?.on?.('addfeature', onAddFeature);
vectorSource?.on?.('changefeature', onChangeFeature);
vectorSource?.on?.('removefeature', onRemoveFeature);
isMounted = true;
};
// Backward-compat: make dispose lightweight so it doesn't freeze the map
const dispose = () => {
isActive = false; // pause reactions
setVisible(false); // hide our labels only
const run = () => { clearAllOverlaysAsync(150); idleClearId = null; };
if (typeof requestIdleCallback === 'function') idleClearId = requestIdleCallback(run, { timeout: 250 });
else idleClearId = setTimeout(run, 180);
};
// Explicit lifecycle controls
const enable = () => {
isActive = true;
setVisible(true);
scheduleSync();
};
const disable = ({ clear = false, delayed = true } = {}) => {
isActive = false;
setVisible(false);
if (clear) {
const run = () => { clearAllOverlaysAsync(150); idleClearId = null; };
if (delayed && typeof requestIdleCallback === 'function') idleClearId = requestIdleCallback(run, { timeout: 250 });
else idleClearId = setTimeout(run, delayed ? 180 : 0);
}
};
const destroy = () => {
if (!isMounted) return;
vectorLayer?.un?.('change:visible', onVisibleChange);
vectorSource?.un?.('addfeature', onAddFeature);
vectorSource?.un?.('changefeature', onChangeFeature);
vectorSource?.un?.('removefeature', onRemoveFeature);
isMounted = false;
if (rafId) cancelAnimationFrame(rafId), rafId = null;
if (idleClearId) { (typeof cancelIdleCallback === 'function' ? cancelIdleCallback : clearTimeout)(idleClearId); idleClearId = null; }
clearAllOverlaysAsync(200);
};
const setFormatter = (nextFormatter) => { currentFormatter = nextFormatter; syncOverlays(); };
const setVisible = (visible) => {
const display = visible ? 'block' : 'none';
for (const el of overlayElementMap.values()) {
try { if (el && el.style) el.style.display = display; } catch {}
}
};
return { mount, dispose, enable, disable, destroy, setFormatter, syncOverlays, setVisible };
}
@@ -594,3 +594,50 @@ export const isMobile = () => {
return width < GLOBAL.MOBILE_SIZE;
};
export const getFeatureLabelCoordinate = (feature) => {
const geometry = feature?.getGeometry?.();
if (!geometry || typeof geometry.getType !== 'function') return null;
const type = geometry.getType();
// 1) 기본 타입
if (type === 'Point') return geometry.getCoordinates();
if (type === 'Polygon') return geometry.getInteriorPoint().getCoordinates();
if (type === 'LineString') return geometry.getCoordinateAt(0.5);
// 2) 멀티 타입들: 가능한 한 대표 좌표를 추출
if (type === 'MultiPoint') {
const coords = geometry.getCoordinates?.();
return (coords && coords[0]) ? coords[0] : null;
}
if (type === 'MultiLineString') {
try { return geometry.getLineString?.(0)?.getCoordinateAt?.(0.5) ?? null; } catch {}
}
if (type === 'MultiPolygon') {
try { return geometry.getInteriorPoints?.()?.getFirstCoordinate?.() ?? null; } catch {}
}
// 3) 기타: extent 중심점
try {
const extent = geometry.getExtent?.();
if (extent) return [
(extent[0] + extent[2]) / 2,
(extent[1] + extent[3]) / 2,
];
} catch {}
return null;
};
// 수는 “좌표를 문자열 키로 변환하는 함수”를
//좌표 정밀도(소수점 자리수)를 조절해서 근사한 좌표를 같은 그룹으로 취급할 수 있게 함.
export const buildCoordinateKeyFactory =(precision)=> {
const p = Math.max(0, Math.min(12, Number.isFinite(precision) ? precision : 6));
return function getCoordinateKey(coordinate) {
const x = coordinate[0];
const y = coordinate[1];
return `${x.toFixed(p)}_${y.toFixed(p)}`;
};
}