/** * Openlayers Map * author: khy * date: 23.09.07 * */ const proj = { viewProjection: "EPSG:3857", WGS84Projection: "EPSG:4326", }; Object.freeze(proj); Array.prototype.toWGS84 = function () { if (this.length === 2) { //coordinate return ol.proj.transform(this, proj.viewProjection, proj.WGS84Projection); } else if (this.length === 4) { //extent return ol.proj.transformExtent(this, proj.viewProjection, proj.WGS84Projection); } else { return this; } }; Array.prototype.toEPSG3857 = function () { if (this.length === 2) { //coordinate return ol.proj.transform(this, proj.WGS84Projection, proj.viewProjection); } else if (this.length === 4) { //extent return ol.proj.transformExtent(this, proj.WGS84Projection, proj.viewProjection); } else { return this; } }; ol.Map.prototype.getProxyURL = function (url) { return `${document.location.protocol}//${document.location.host}/proxy/?${url ? url : ""}`; }; ol.Map.prototype.addBaseTileLayer = function (options) { let host = 'gis.korail.com'; function createBaseTileLayer(map) { const proxy = map.getProxyURL(); const base = new ol.layer.Tile({ name: "base", source: new ol.source.XYZ({ tileUrlFunction: (zxy) => { const [z, x, y] = zxy; if (z < 5 || z > 15) { return; } return `https://${host}/tilemap/background/${z}/${x}/${y}.png`; }, minZoom: 5, maxZoom: 15, }), zIndex: 0, }); if (options === "korean") { const label = new ol.layer.Tile({ name: "base-label", source: new ol.source.XYZ({ tileUrlFunction: (zxy) => { const [z, x, y] = zxy; if (z < 5 || z > 15) { return; } return `https://${host}/tilemap/korean/${z}/${x}/${y}.png`; }, minZoom: 5, maxZoom: 15, }), zIndex: 0, }); const layergroup = new ol.layer.Group({ layers: [base, label], }); return layergroup; } else { return base; } } const layer = createBaseTileLayer(this); this.addLayer(layer); return layer; }; ol.Map.prototype.createBaseLayerButton = function () { const mapElement = this.getTargetElement(); if (!mapElement.querySelector("button[usage=base]")) { const toggleButton = document.createElement("button"); toggleButton.setAttribute("usage", "base"); toggleButton.textContent = ""; toggleButton.style.position = "absolute"; toggleButton.style.top = "0px"; toggleButton.style.left = "0px"; toggleButton.style.margin = "10px"; toggleButton.style.marginLeft = "42px"; toggleButton.style.width = "30px"; toggleButton.style.height = "30px"; toggleButton.style.borderRadius = "50%"; toggleButton.style.border = "1px solid gray"; toggleButton.style.background = "white"; toggleButton.style.color = "black"; toggleButton.style.textAlign = "center"; let status = false; const toggleLayer = () => { this.getAllLayers() .filter((l) => l.get("name") !== "base" && l.get("name") !== "base-label" && l.get("name") !== "grid-tile") .forEach((l) => { l.setVisible(status); }); status = !status; toggleButton.style.background = status ? "dodgerblue" : "white"; toggleButton.style.color = status ? "white" : "black"; if (status) { viewChange(null, null); map.getView().setConstrainResolution(true); } else { map.getView().setConstrainResolution(false); viewChange(0, document.querySelector("ul.notice > li")); } }; toggleButton.onclick = toggleLayer; let zoomLevel = undefined; this.on("moveend", (view) => { const nowZoomLevel = Number(this.getView().getZoom().toFixed()); if (zoomLevel !== nowZoomLevel) { zoomLevel = nowZoomLevel; toggleButton.textContent = zoomLevel; } }); mapElement.appendChild(toggleButton); } }; ol.Map.prototype.createGridTiles = function () { const mapElement = this.getTargetElement(); if (!mapElement.querySelector("button[usage=grid-tile]")) { const toggleButton = document.createElement("button"); toggleButton.setAttribute("usage", "grid-tile"); toggleButton.textContent = "#"; toggleButton.style.position = "absolute"; toggleButton.style.top = "0px"; toggleButton.style.left = "0px"; toggleButton.style.margin = "10px"; toggleButton.style.width = "30px"; toggleButton.style.height = "30px"; toggleButton.style.borderRadius = "50%"; toggleButton.style.border = "1px solid gray"; toggleButton.style.background = "white"; toggleButton.style.color = "black"; toggleButton.style.textAlign = "center"; const toggleLayer = () => { const layer = this.getLayer("grid-tile"); if (layer) { const currentVisible = layer.getVisible(); const postVisible = !currentVisible; this.getAllLayers() .filter((l) => l.get("name") !== "base" && l.get("name") !== "base-label" && l.get("name") !== "grid-tile") .forEach((l) => { l.setVisible(currentVisible); }); layer.setVisible(postVisible); toggleButton.style.background = postVisible ? "dodgerblue" : "white"; toggleButton.style.color = postVisible ? "white" : "black"; if (postVisible) { viewChange(null, null); } else { viewChange(0, document.querySelector("ul.notice > li")); } } }; toggleButton.onclick = toggleLayer; mapElement.appendChild(toggleButton); } if (!this.getLayer("grid-tile")) { const tileSize = 256; const canvas = document.createElement("canvas"); canvas.width = tileSize; canvas.height = tileSize; const context = canvas.getContext("2d"); context.strokeStyle = "gray"; context.textAlign = "center"; const lineHeight = 30; context.font = `${lineHeight - 6}px sans-serif`; const layer = new ol.layer.WebGLTile({ name: "grid-tile", source: new ol.source.DataTile({ loader: (z, x, y) => { const half = tileSize / 2; context.clearRect(0, 0, tileSize, tileSize); // context.fillStyle = 'rgba(255, 255, 255, 0.7)'; // context.fillRect(0, 0, tileSize, tileSize); context.fillStyle = "black"; context.fillText(`z: ${z}`, half, half - lineHeight); context.fillText(`x: ${x}`, half, half); context.fillText(`y: ${y}`, half, half + lineHeight); context.strokeRect(0, 0, tileSize, tileSize); const data = context.getImageData(0, 0, tileSize, tileSize).data; return new Uint8Array(data.buffer); }, }), zIndex: 999, }); layer.setVisible(false); this.addLayer(layer); } }; ol.source.Vector.fromGeoJSON = function (geojson) { return new ol.source.Vector({ features: new ol.format.GeoJSON().readFeatures(geojson, { featureProjection: proj.viewProjection }), format: new ol.format.GeoJSON(), strategy: ol.loadingstrategy.bbox, }); }; ol.Map.prototype.addRegionalLayer = async function () { const regional_lines = new Array(8); async function createRegionalLine(url, index) { return new Promise(async (resolve, reject) => { try { const response = await fetch(url); const json = await response.json(); insertLine(json, index); resolve(); } catch { reject(); } }); } // 권역 라인이 포인트 형식으로 되어있어서 라인형식으로 바꾸고 데이터 교체하는 함수 function insertLine(json, i) { const coordinates = json.features.map((point) => { const converted = point.geometry.coordinates.toEPSG3857(); return converted; }); const geomLineString = new ol.geom.LineString(coordinates); const featureLineString = new ol.Feature({ geometry: geomLineString }); regional_lines[i] = featureLineString; } // $.getJOSN 으로 json 데이터 읽어 드리고 insertLine 은 현재 json 파일이 line 형식이 아닌 포인트 // 형식으로 // 만들었음 // 포인트 형식을 만든 이유는 수정시 포인트 형식이 필요해서 우선 포인트 형식으로 저장 await Promise.all([ createRegionalLine("resource/Regional_point_chungbuk.json", 0), createRegionalLine("resource/Regional_point_gang.json", 1), createRegionalLine("resource/Regional_point_seoul_east.json", 2), createRegionalLine("resource/Regional_point_seoul_west.json", 3), createRegionalLine("resource/Regional_point_daejeon_chungnam.json", 4), createRegionalLine("resource/Regional_point_jeonbuk.json", 5), createRegionalLine("resource/Regional_point_jeonnam.json", 6), createRegionalLine("resource/Regional_point_daegu.json", 7), ]); const regionalSource = new ol.source.Vector({ features: regional_lines, }); this.addVectorLayer({ name: "Regional_Line", source: regionalSource, style: layerStyles.regional, zIndex: 0, }); }; /** * WebGL Layer * author: khy */ const canvas = document.createElement("canvas"); const gl = canvas?.getContext("webgl") || canvas?.getContext("experimental-webgl"); const webglAvailable = gl instanceof WebGLRenderingContext; class WebGLVectorLayer extends ol.layer.Layer { constructor(params) { super(params); this.style = params.style; } createRenderer() { const style = this.style; return new ol.renderer.webgl.VectorLayer(this, { style, }); } } function VectorLayer(params) { return new ol.layer.Vector(params); //return (webglAvailable) ? new WebGLVectorLayer(params) : new ol.layer.Vector(params); } function PointsLayer(params) { const hasStyleFunction = typeof params?.style === "function"; return (webglAvailable && !hasStyleFunction) ? new ol.layer.WebGLPoints(params) : new ol.layer.Vector(params); } /** * ol.Map prototype 확장 * @param ...args * @returns */ ol.Map.prototype.addLayerWithType = async function (...args) { const LayerType = args[0]; args = args[1]; return new Promise(async (resolve, reject) => { try { let url, fetchOption, layerOption; let response, geojson; switch (args.length) { case 3: if ( typeof args[0] === "string" && //fetch url typeof args[1] === "object" && //fetch option typeof args[2] === "object" ) { //layer option (url = args[0]), (fetchOption = args[1]), (layerOption = args[2]); response = await fetch(url, fetchOption); geojson = await response.json(); layerOption.source = await ol.source.Vector.fromGeoJSON(geojson); } else { reject(); } break; case 2: if ( typeof args[0] === "string" && //fetch url typeof args[1] === "object" ) { //layer option (url = args[0]), (layerOption = args[1]); console.log(`addLayerWithType(${url})`, fetch); response = await fetch(url); geojson = await response.json(); console.log(geojson); layerOption.source = await ol.source.Vector.fromGeoJSON(geojson); } else if ( typeof args[0] === "object" && //layer option typeof args[1] === "object" ) { //map object layerOption = args[0]; } else { reject(); } break; case 1: //layer option default: layerOption = args[0]; } if (layerOption.labelStyleFunction) { const layergroup = new ol.layer.Group({ layers: [ new LayerType({ ...layerOption, source: layerOption.source || new ol.source.Vector(), style: layerOption.style || ol.style.flat.createDefaultStyle(), }), new ol.layer.Vector({ name: layerOption.name ? `${layerOption.name}_label` : "label", source: layerOption.source || new ol.source.Vector(), style: layerOption.labelStyleFunction, declutter: true, zIndex: layerOption.zIndex + 1, }), ], }); this.addLayer(layergroup); resolve(layergroup); } else { const layer = new LayerType({ ...layerOption, source: layerOption.source || new ol.source.Vector(), style: layerOption.style || ol.style.flat.createDefaultStyle(), }); this.addLayer(layer); resolve(layer); } } catch { reject(); } }); }; /** * @author khy * @description WebGL벡터레이어나 일반벡터레이어를 생성하여 map객체에 addLayer()함 * 전달인자를 1개, 2개, 3개를 줄 수 있음. * 전달인자 1개: layer option * 전달인자 2개: fetch url, layer option * 전달인자 3개: fetch url, fetch option, layer option * @param ...args * case (args0) Layer option * case (args0, args1) fetch url, layer option || layer option, map target * case (args0, args1, args2) fetch url, fetch option, layer option */ /** * @returns * ol.layer.vector || WebGLVectorLayer */ ol.Map.prototype.addVectorLayer = async function (...args) { return new Promise(async (resolve) => { resolve(this.addLayerWithType(VectorLayer, args)); }); }; ol.Map.prototype.addRailLayer = async function (...args) { return new Promise(async (resolve) => { resolve(this.addLayerWithType(ol.layer.Vector, args)); }); }; /** * @returns * ol.layer.vector || ol.layer.WebGLPoints */ ol.Map.prototype.addPointsLayer = async function (...args) { return new Promise(async (resolve) => { resolve(this.addLayerWithType(PointsLayer, args)); }); }; ol.Map.prototype.addNormalRailLayer = function (options) { return this.addVectorLayer( "resource/NormalLine.json", Object.assign( { name: "NormalLine", style: { "stroke-width": 2, }, zIndex: 2, }, options ) ); }; ol.Map.prototype.addNormalStationLayer = function (options) { return this.addPointsLayer( "resource/NormalStation.json", Object.assign( { name: "NormalStation", style: { "circle-radius": 5, "circle-stroke-width": 3, "circle-fill-color": "gainsboro", }, zIndex: 3, }, options ) ); }; ol.Map.prototype.addKTXRailLayer = function (options) { return this.addVectorLayer( "resource/KTXLine.json", Object.assign( { name: "KTXLine", style: { "stroke-color": "#005BAC", "stroke-width": 5, }, zIndex: 1, }, options ) ); }; ol.Map.prototype.addKTXStationLayer = function (options) { return this.addPointsLayer( "resource/KTXStation.json", Object.assign( { name: "KTXStation", style: { "circle-radius": 7, "circle-stroke-color": "#005BAC", "circle-stroke-width": 3, "circle-fill-color": "gainsboro", }, zIndex: 4, }, options ) ); }; ol.Map.prototype.addBaseRailLayer = function () { this.addNormalRailLayer(); this.addKTXRailLayer(); }; ol.Map.prototype.addBaseStationLayer = function () { this.addNormalStationLayer(); this.addKTXStationLayer(); }; /* function removeLayer(layername) { getLayers(layername).forEach((layer) => { layer.dispose(); map.removeLayer(layer); }) } */ ol.Map.prototype.getLayer = function (layername) { return this.getLayers(layername)[0]; }; ol.Map.prototype.getLayers = function (layername) { return layername ? this.getAllLayers().filter((layer) => layer.get("name") === layername) : this.getLayerGroup().getLayers(); }; ol.Map.prototype.flyTo = function (params) { this.once("postrender", () => { this.flyToNow(params); }); }; ol.Map.prototype.flyToNow = function (params) { const convertedCenter = params.center.toEPSG3857(); const option = Object.assign(params, { center: convertedCenter, duration: 700, easing: ol.easing.easeOut, }); this.getView().animate(option); }; ol.Map.prototype.setVisibleLayer = function (layername, visible) { this.getLayers(layername).forEach((layer) => { layer.setVisible(visible); }); }; /** * Overlay * author: khy * date: 23.09.07 * */ ol.Map.prototype.createOverlay = function () { if (!document.getElementById("popup")) { const popupElement = document.createElement("div"); popupElement.id = "popup"; this.getTargetElement().appendChild(popupElement); } const popup = new ol.Overlay({ id: "popup", element: document.getElementById("popup"), autoPan: true }); this.addOverlay(popup); return popup; }; ol.Map.prototype.getOverlay = function () { const popup = this.getOverlayById("popup") || this.createOverlay(); return popup; }; ol.Map.prototype.showOverlay = function () { this.getOverlay().element.style.display = null; }; ol.Map.prototype.hideOverlay = function () { this.getOverlay().element.style.display = "none"; }; ol.Map.prototype.isShowingOverlay = function () { return (map.getOverlay().element.style.display !== "none") } /** * Drag Interaction * author: khy * param * options : { * layerFilter: function(default=undefined), * hitTolerance: number(default=0), * checkWrapped: boolean(default=true), * } * usage * map.addInteraction(new Drag( options | null )); * */ class Drag extends ol.interaction.Pointer { constructor(options) { super({ handleDownEvent: handleDownEvent, handleDragEvent: handleDragEvent, handleMoveEvent: handleMoveEvent, handleUpEvent: handleUpEvent, }); this.options_ = options; this.coordinate_ = null; this.cursor_ = "grabbing"; this.feature_ = null; this.previousCursor_ = undefined; } } function handleDownEvent(evt) { const map = evt.map; const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature, this.options_); if (feature) { this.coordinate_ = evt.coordinate; this.feature_ = feature; } return !!feature; } function handleDragEvent(evt) { const deltaX = evt.coordinate[0] - this.coordinate_[0]; const deltaY = evt.coordinate[1] - this.coordinate_[1]; const geometry = this.feature_.getGeometry(); geometry.translate(deltaX, deltaY); this.coordinate_[0] = evt.coordinate[0]; this.coordinate_[1] = evt.coordinate[1]; } function handleMoveEvent(evt) { if (this.cursor_) { const map = evt.map; const feature = map.forEachFeatureAtPixel(evt.pixel, (feature) => feature, this.options_); const element = evt.map.getTargetElement(); if (feature) { if (element.style.cursor != this.cursor_) { this.previousCursor_ = element.style.cursor; element.style.cursor = this.cursor_; } } else if (this.previousCursor_ !== undefined) { element.style.cursor = this.previousCursor_; this.previousCursor_ = undefined; } } } function handleUpEvent() { this.coordinate_ = null; this.feature_ = null; return false; } /** * loading spinner * author: khy * start * mapLoadStart(); * end * mapLoadEnd(); */ ol.Map.prototype.addLoadingEffect = function (option) { const css = ` @keyframes spinner { to { transform: rotate(360deg); } } #map { background: linear-gradient(to top left, #005bac, #3a3a3f); } ${option?.transparent ? "/*" : ""} #map > .ol-viewport { display: none; } ${option?.transparent ? "*/" : ""} ${option?.background ? "" : "/*"} #map.spinner::before { content: ""; position: absolute; width: 100%; height: 100%; background: ${option?.background}; z-index:8; } ${option?.background ? "" : "*/"} #map.spinner::after { content: ""; box-sizing: border-box; position: absolute; top: 50%; left: 50%; width: 40px; height: 40px; margin-top: -20px; margin-left: -20px; border-radius: 50%; border: 4px solid ${option?.color ? option.color : "white"}; border-top-color: transparent; animation: spinner 0.6s linear infinite; z-index:9; } `; const head = document.head || document.getElementsByTagName("head")[0]; if (head.querySelector("style[usage=spinner]")) { return; } const style = document.createElement("style"); style.setAttribute("usage", "spinner"); style.type = "text/css"; if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } head.appendChild(style); }; ol.Map.prototype.removeLoadingEffect = function () { const head = document.head || document.getElementsByTagName("head")[0]; head.querySelector("style[usage=spinner]")?.remove(); }; ol.Map.prototype.startLoadingEffect = function (option) { this.addLoadingEffect(option); this.getTargetElement().classList.add("spinner"); }; ol.Map.prototype.finishLoadingEffect = function () { this.removeLoadingEffect(); this.getTargetElement().classList.remove("spinner"); };