Files
frontend/packages/common/business_components/src/ui/map/lib/gis.layer.js
T
2026-08-26 16:25:29 +09:00

851 lines
24 KiB
JavaScript

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';
import { showBusinessUserMessage } from '../../../lib/user-message';
import {
buildFloodDngrSldBody,
FLOOD_DNGR_MAP_LAYER_TYPE,
getFloodDngrSldSettings,
} from './gis.wms-sld.js';
/**
* GisApp.layer
* @requires gis.core.js
* @requires gis.style.js
*/
const GisOlLayer = function (options) {
this.initialize(options);
};
GisOlLayer.prototype = {
layers: [],
cesiumLayers: [],
searchLayer: null,
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);
//스타일 추가
returnLayer.setStyle(this.gisStyle.getLayerStyleFunction(returnLayer, options));
returnLayer.set('id', options.id + '');
this.layers.push(returnLayer);
return returnLayer;
},
/**
* gisStyle 반환
*/
getGisStyle: function () {
return this.gisStyle;
},
/**
* [사용]WFS 벡터 소스 반환
*/
getVectorSourceBbox: function (options) {
let url = GisApp.GEOSERVER_URL;
let toProj = GisApp.TO_PROJ;
let lyrTyNm = options.lyrTyNm;
let typename = this.storageNm + lyrTyNm;
const params = {};
params['TYPENAME'] = typename;
params['SERVICE'] = 'WFS';
params['VERSION'] = '1.1.0';
params['REQUEST'] = 'GetFeature';
params['SRS'] = toProj;
params['OUTPUTFORMAT'] = 'application/json';
let source = new VectorSource({
format: new GeoJSON(),
url: function (extent) {
const [minX, minY, maxX, maxY] = extent;
const bboxFilter = `BBOX(geom, ${minX}, ${minY}, ${maxX}, ${maxY}, '${toProj}')`;
const filter = options?.flterSql && options.flterSql && options.flterSql.trim() !== ''
? `(${options.flterSql}) AND ${bboxFilter}`
: bboxFilter;
params['CQL_FILTER'] = filter;
const strParams = new URLSearchParams(params).toString();
return url + '?' + strParams;
},
strategy: bbox,
});
return source;
},
/**
* [사용]WFS 벡터 소스 반환
*/
getVectorSourceAll: function (options) {
let url = GisApp.GEOSERVER_URL;
let toProj = GisApp.TO_PROJ;
let lyrTyNm = options.lyrTyNm;
let typename = this.storageNm + lyrTyNm;
const filter = (options?.flterSql ?? '').trim();
let params = {};
params['TYPENAME'] = typename;
params['SERVICE'] = 'WFS';
params['VERSION'] = '1.1.0';
params['REQUEST'] = 'GetFeature';
params['SRS'] = toProj;
params['OUTPUTFORMAT'] = 'application/json';
if (filter) params['CQL_FILTER'] = filter;
else if (GisApp.DROUGHT_DATES) {
params['START_YMD'] = GisApp.DROUGHT_DATES.startYmd;
params['END_YMD'] = GisApp.DROUGHT_DATES.endYmd;
}
let strParams = new URLSearchParams(params).toString();
let source = new VectorSource({
format: new GeoJSON(),
url: function (extent) {
return url + '?' + strParams;
},
strategy: all,
});
return source;
},
/**
* [사용]WFS 레이어 생성
*/
getWfsLayer: function (option) {
option = option || {};
//레이어 생성
let returnLayer = this.makeLayer(option);
//레이어 소스 생성
let source;
let cmncNm = option.cmncNm;
try {
if (cmncNm == 'bbox') {
source = this.getVectorSourceBbox(option);
} else {
source = this.getVectorSourceAll(option);
}
} catch (error) {
console.warn(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;
const filter = String(options?.flterSql ?? '').trim();
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['TRANSPARENT'] = true;
params['TILED'] = true;
params['STYLES'] = '';
if (filter) params['CQL_FILTER'] = filter;
if (lyrTyNm === FLOOD_DNGR_MAP_LAYER_TYPE) {
const sldSettings = getFloodDngrSldSettings(GisApp.layerCode, options?.id);
const sldBody = buildFloodDngrSldBody(lyrTyNm, sldSettings);
if (!sldBody) {
console.warn('[GisApp] 도시침수지도 WMS SLD 생성에 필요한 침수심도 설정 5건이 유효하지 않습니다.', options);
return null;
}
params['SLD_BODY'] = sldBody;
}
const wmsLayer = new Tile({
...layerOptions,
id: layerOptions?.id,
source: new TileWMS({
url,
params,
serverType: 'geoserver',
}),
});
wmsLayer.set('id', String(layerOptions?.id));
wmsLayer.setZIndex(Number(options?.drawOrdr) || 1);
wmsLayer.setMinZoom(Number(options?.minViewSize) || 0);
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: bbox,
});
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 = this.getOpenLayersZoom(cesiumScene);
cesiumLayer.show = zoomLevel >= Number(properties?.minZoom);
} else {
cesiumLayer.show = false;
}
}
});
if (me.cesiumLayers.length > 0) {
cesiumScene._moveEndListener = () => {
const zoomLevel = this.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);
}
},
getFeatureById: function (featureId) {
let returnFeature;
this.layers.getArray().some(function (layer) {
const source = layer.getSource();
if (source instanceof VectorSource) {
const feature = source.getFeatureById(featureId);
if (feature) {
returnFeature = feature;
return true;
}
}
});
return returnFeature;
},
getOpenLayersZoom: function (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)));
},
ensureSearchLayer: function (option) {
let layer = this.searchLayer;
if (!layer) {
let source;
try {
if (option.cmncNm == 'bbox') {
source = this.getVectorSourceBbox(option);
} else {
source = this.getVectorSourceAll(option);
}
} catch (error) {
console.warn(option);
}
layer = new VectorImageLayer({
...option,
id: 'search-layer',
source: source,
zIndex: 9999
});
layer.setStyle(this.gisStyle.getLayerStyleFunction(layer, option));
let map = GisApp.Module.core.map;
map.addLayer(layer);
this.searchLayer = layer;
}
return layer;
},
clearSearchLayer: function (option) {
const layer = this.ensureSearchLayer(option);
layer.getSource().clear(true);
if (this.searchLayer) {
GisApp.Module.core.map.removeLayer(this.searchLayer);
this.searchLayer = null;
}
},
showSearchResultsFromGeoJSON: function (lyrTyNm, featureId) {
//레이어 타입 설정(point, line 등. 이걸 style에서 사용)
const layerCode = GisApp.layerCode;
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
// 이전 검색 레이어 완전히 제거
this.clearSearchLayer(matched);
// 새로운 검색 레이어 생성
const layer = this.ensureSearchLayer(matched);
layer.set("gobjTy", matched.gobjTy);
// 새 소스 만들기
const source = this.getFeatureSourceFromGeoserver(lyrTyNm, featureId, matched);
// 1) 로드 완료시 extent 맞추기
const onLoadEnd = () => {
if (Object.keys(source.nullGeometryFeatures_).length > 0) showBusinessUserMessage('해당 시설의 위치 정보가 없습니다.');
const feats = source.getFeatures();
if (feats && feats.length > 0) {
const select = GisApp.Module.select;
select.selectFeature(feats[0], false);
} else {
console.warn('[search-layer] GeoJSON에 피처가 없습니다.');
}
// 한번만 쓰고 이벤트 정리
source.un('featuresloadend', onLoadEnd);
source.un('featuresloaderror', onLoadError);
};
const onLoadError = (e) => {
console.error('[search-layer] GeoJSON 로드 실패:', e);
source.un('featuresloadend', onLoadEnd);
source.un('featuresloaderror', onLoadError);
};
// 이벤트를 먼저 걸고 → setSource
source.once('featuresloadend', onLoadEnd);
source.once('featuresloaderror', onLoadError);
// 레이어에 소스 적용(이때부터 로딩 시작)
layer.setSource(source);
// 필요시 명시적으로 재요청
source.refresh();
},
/**
* 피쳐 ID로 지오서버에서 검색해서 피처의 벡터소스를 리턴
* @param {} lyrTyNm
* @param {*} featureId
* @returns
*/
getFeatureSourceFromGeoserver: function (lyrTyNm, featureId, option) {
const typeName = this.storageNm + lyrTyNm;
const srsName = GisApp.TO_PROJ;
const baseUrl = GisApp.GEOSERVER_URL;
const source = new VectorSource({
...option,
format: new GeoJSON(),
url: () => {
const params = new URLSearchParams({
SERVICE: 'WFS',
VERSION: '1.1.0',
REQUEST: 'GetFeature',
TYPENAME: typeName,
SRSNAME: srsName,
OUTPUTFORMAT: 'application/json',
FEATUREID: featureId,
});
return `${baseUrl}?${params.toString()}`;
},
strategy: all,
});
return source;
},
/**
* 여러 개의 검색 결과를 GeoJSON으로 표시
* @param {string} lyrTyNm 레이어 타입명
* @param {Array|string} featureIds 피처 ID 배열 또는 단일 ID
*/
showMultipleSearchResultsFromGeoJSON: function (lyrTyNm, featureIds) {
const layerCode = GisApp.layerCode;
const matched = Object.values(layerCode).find(obj => obj.lyrTyNm === lyrTyNm);
// 이전 검색 레이어 완전히 제거
this.clearSearchLayer(matched);
// 새로운 검색 레이어 생성
const layer = this.ensureSearchLayer(matched);
layer.set("gobjTy", matched.gobjTy);
const source = this.getMultiFeatureSourceFromGeoserver(lyrTyNm, featureIds);
const onLoadEnd = async () => {
const feats = source.getFeatures();
if (feats && feats.length > 0) {
const select = GisApp.Module.select;
await select.resetSelectedFeature();
for (const feature of feats) select.selectMultiFeature(feature, true);
const geometries = feats.map(f => f.getGeometry());
if (geometries.length > 0) {
const extent = this.combineExtents(geometries.map(g => g?.getExtent()));
let map = GisApp.Module.core.map;
const targetZoom = 16;
const currentZoom = Number(map.getView().getZoom());
const maxZoom = Number.isFinite(currentZoom) && currentZoom > targetZoom ? currentZoom : targetZoom;
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: maxZoom
});
}
} else {
console.warn('[search-layer] GeoJSON에 피처가 없습니다.');
}
source.un('featuresloadend', onLoadEnd);
source.un('featuresloaderror', onLoadError);
};
const onLoadError = (e) => {
console.error('[search-layer] GeoJSON 로드 실패:', e);
source.un('featuresloadend', onLoadEnd);
source.un('featuresloaderror', onLoadError);
};
source.once('featuresloadend', onLoadEnd);
source.once('featuresloaderror', onLoadError);
layer.setSource(source);
source.refresh();
},
/**
* 피쳐 ID로 지오서버에서 검색해서 여러개 피처의 벡터소스를 리턴
* @param {} lyrTyNm
* @param {*} featureId
* @returns
*/
getMultiFeatureSourceFromGeoserver: function (lyrTyNm, featureId) {
const typeName = this.storageNm + lyrTyNm;
const srsName = GisApp.TO_PROJ;
const baseUrl = GisApp.GEOSERVER_URL;
const ids = Array.isArray(featureId) ? featureId : [featureId];
const source = new VectorSource({
format: new GeoJSON(),
url: () => {
const params = new URLSearchParams({
SERVICE: 'WFS',
VERSION: '1.1.0',
REQUEST: 'GetFeature',
TYPENAME: typeName,
SRSNAME: srsName,
OUTPUTFORMAT: 'application/json',
FEATUREID: ids.join(','),
});
return `${baseUrl}?${params.toString()}`;
},
strategy: all,
});
return source;
},
/**
* 여러 extent 배열을 받아 결합된 extent를 반환
* @param {Array<Array<number>>} extents [[minX,minY,maxX,maxY], ...]
* @returns {Array<number>|null}
*/
combineExtents: function (extents) {
if (!extents || extents.length === 0) return null;
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const e of extents) {
if (!e || e.length < 4) continue;
minX = Math.min(minX, e[0]);
minY = Math.min(minY, e[1]);
maxX = Math.max(maxX, e[2]);
maxY = Math.max(maxY, e[3]);
}
if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) return null;
return [minX, minY, maxX, maxY];
}
};
export default GisOlLayer;