851 lines
24 KiB
JavaScript
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;
|