document.addEventListener("DOMContentLoaded", async function () { initMap(); await initLayer(); initView(); // initMapLegend(); mouseEvent(); }); let map; let originalFetch = window.fetch; async function newFetch(url, options = {}) { let headers = options.headers || { 'user-agent': "Mozilla/5.0 (Linux; Android 12; SM-N976N Build/SP1A.210812.016; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.133 Mobile Safari/537.36 korailtalk AppVersion/6.3.3", 'x-requested-with': "com.korail.talk", 'sec-ch-ua-platform': "Android", "referer": "https://gis.korail.com/korailTalk/entrance" }; let f = originalFetch('https://proxy.devpg.net', { headers: { 'X-Proxy-URL': 'https://gis.korail.com' + url, 'X-Proxy-Header': JSON.stringify(headers) } }); console.log(`fetch(https://gis.korail.com${url}, headers=${JSON.stringify(headers)}`, f); return f } window.fetch = newFetch; function initMap() { map = new ol.Map({ target: 'map', view: new ol.View({ minZoom: 5, maxZoom: 15.9, extent: [120, 30, 135, 45].toEPSG3857(), }), controls: ol.control.defaults.defaults({ attribution: false, zoom: false, rotate: false, }), interactions: ol.interaction.defaults.defaults({ altShiftDragRotate: false, pinchRotate: false, }).extend([new ol.interaction.DblClickDragZoom()]) }); if (params.lon && params.lat) { map.getView().setCenter([params.lon, params.lat].toEPSG3857()); map.getView().setZoom(11); } else { map.getView().setCenter([127.35, 36.50].toEPSG3857()); map.getView().setZoom(6); } map.getTargetElement().style.background = '#b5dbf3'; } const trainColors = { ktx: '#1B4298', itx: '#C10230', etc: '#54565A', srt:'#651C5C' } Object.freeze(trainColors); const layerStyles = { line: { 'stroke-width': 2.25, 'stroke-color': '#68a7d5', }, station: { 'circle-radius': [ 'match', ['get', 'grade', 'string'], '0', ["interpolate", ["linear"],["zoom"], 5, 3, 15, 6], '1', ["interpolate", ["linear"],["zoom"], 5, 3, 15, 6], '2', ["interpolate", ["linear"],["zoom"], 5, 2, 15, 5], '3', ["interpolate", ["linear"],["zoom"], 5, 2, 15, 5], 0 ], 'circle-stroke-width': [ 'match', ['get', 'grade', 'string'], '0', ["interpolate", ["linear"],["zoom"], 5, 1, 15, 3], '1', ["interpolate", ["linear"],["zoom"], 5, 1, 15, 3], '2', ["interpolate", ["linear"],["zoom"], 5, 1, 15, 3], '3', 0, 0 ], 'circle-stroke-color': '#68a7d5', 'circle-fill-color': [ 'match', ['get', 'grade', 'string'], '3', '#68a7d5', 'white' ] }, station_label: { 'text-value': ['get', 'name', 'string'], 'text-stroke-color': "#68a7d5", 'text-stroke-width': 3, 'text-fill-color': "#ffffff", 'text-font': [ 'match', ['get', 'grade', 'number'], 0, '17px SUIT-M', 1, '15px SUIT-M', 2, '13px SUIT-M', 3, '11px SUIT-M', '0px SUIT-M' ], 'text-offset-x': ['get', 'text_offset_x'], 'text-offset-y': ['get', 'text_offset_y'], }, trainStyle: (feature, resolution) => { const { trn_no: trnNo, trn_case: trnCase, trn_opr_cd: trnOpr, trn_clsf: trnClass, icon, bearing } = feature.getProperties(); const coords = feature.getGeometry().getCoordinates() const overlabs = map.getLayer('train').getSource().getFeaturesAtCoordinate(coords).sort((a, b) => a.get('trn_no') - b.get('trn_no')); return new ol.style.Style({ image: new ol.style.Icon({ src: `icon/train/ic_${icon}.svg`, rotation: bearing, scale: 0.6 + ((map.getView().getZoomForResolution(resolution) - 10) / 10), declutterMode: 'none', }), text: (map.getView().getZoom() >= 11) ? new ol.style.Text({ font: '14px SUIT-B', stroke: new ol.style.Stroke({ color: '#f3f3f3', width: 1, }), fill: new ol.style.Fill({ color: trainColors[trnClass], }), offsetX: 5 + map.getView().getZoom(), offsetY: 0 + overlabs.indexOf(feature) * 20, overflow: true, declutterMode: 'none', textAlign: 'left', textBaseline: 'middle', text: `${trnCase} ${trnNo}`, }) : null, zIndex: map.get('trainClicked')?.getId() == feature.getId() ? Infinity : trnOpr == 15 ? overlabs.reverse().indexOf(feature) : 0 }) } }; Object.freeze(layerStyles); async function initLayer() { try { map.startLoadingEffect(); await Promise.all([ map.addBaseTileLayer('korean'), initRailLayers(), initStationLayers(), initTrainLayer(), ]); } catch { } finally { map.finishLoadingEffect(); } } async function initRailLayers() { return new Promise(async (resolve) => { await Promise.all([ // 고속선 정보 map.addRailLayer('/api/data/rail_ktx', { name: 'rail_ktx', style: layerStyles.line, zIndex: 1, }), // 준고속선 정보 map.addRailLayer('/api/data/rail_semi', { name: 'rail_semi', style: layerStyles.line, zIndex: 1, }), // 일반선 정보 map.addRailLayer('/api/data/rail_normal', { name: 'rail_normal', style: layerStyles.line, zIndex: 1, }), // 물류선(추가) 정보 map.addRailLayer('/api/data/rail_logis', { name: 'rail_logis', style: layerStyles.line, zIndex: 1, }), ]); resolve(); }) } async function initStationLayers() { const response = await fetch('/api/data/station'); map.set('korailStation', Object.freeze(await response.json())); const filteredStation = (binary) => { const temp = JSON.parse(JSON.stringify(map.get('korailStation'))); temp.features = temp.features.filter((f) => (f.properties.text_offset_x && f.properties.text_offset_y) !== null); temp.features = temp.features.filter((f) => Number(f.properties.shown_layer) & binary); return ol.source.Vector.fromGeoJSON(temp); } return new Promise(async (resolve) => { await Promise.all([ // 고속역 정보 map.addPointsLayer({ name: 'KTX_Station', source: filteredStation(0b100000), style: layerStyles.station, labelStyleFunction: layerStyles.station_label, zIndex: 2, }), // 준고속역 정보 map.addPointsLayer({ name: 'SemiExpress_Station', source: filteredStation(0b010000), style: layerStyles.station, labelStyleFunction: layerStyles.station_label, zIndex: 2, }), // 일반역 정보 map.addPointsLayer({ name: 'Normal_Station', source: filteredStation(0b001000), style: layerStyles.station, labelStyleFunction: layerStyles.station_label, zIndex: 2, }), // 여객역 정보 map.addPointsLayer({ name: 'Passenger_Station', source: filteredStation(0b000100), style: layerStyles.station, labelStyleFunction: layerStyles.station_label, zIndex: 2, }), // 물류역 정보 map.addPointsLayer({ name: 'Dist_Station', source: filteredStation(0b000001), style: layerStyles.station, labelStyleFunction: layerStyles.station_label, zIndex: 2, }), ]); resolve(); }); } function initRefreshDOM() { const dom = document.createElement('div') dom.id = 'refresh-info' dom.style.position = 'absolute' dom.style.bottom = '0px' dom.style.right = '0px' dom.style.display = 'flex' dom.style.flexDirection = 'column' dom.style.justifyContent = 'space-evenly' dom.style.alignItems = 'end' dom.style.margin = '20px' dom.style.padding = 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)' dom.style.boxSizing = 'border-box' dom.style.touchAction = 'none' const button = document.createElement('input') button.type = 'image' button.src = 'icon/refresh.svg' button.alt = '새로고침' button.onclick = (event) => { refreshTrainLayer() const target = event.currentTarget target.classList.add('rotate') setTimeout(() => { target.classList.remove('rotate'); }, 500); } button.style.width = '1rem' button.style.height = '1rem' button.style.background = 'transparent' button.style.border = '0px' button.style.borderRadius = '50%' button.style.boxShadow = '0px 0px 5px gainsboro' button.style.background = 'white' button.style.margin = '5px 0px' button.style.padding = '5px' const text = document.createElement('span') text.style.fontSize = 'small' text.style.transition = 'color 0.5s' text.style.color = 'black' dom.appendChild(button) dom.appendChild(text) map.getTargetElement().appendChild(dom) updateRefreshText(); } const refreshIterator = { interval: undefined, function: undefined, } function syncIterator() { const zoom = map.getView().getZoom(); const intZoom = Math.floor(zoom); const intervalSec = 60 - ((intZoom - 5) * 5.5); refreshIterator.interval = intervalSec * 1000; clearInterval(refreshIterator.function); refreshIterator.function = setInterval(refreshTrainLayer, refreshIterator.interval); } function updateRefreshText() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 +1 필요 const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const text = document.querySelector('div#refresh-info > span') text.textContent = `(${year}.${month}.${day}. ${hours}:${minutes}:${seconds}) 기준`; text.classList.add('blink') setTimeout(() => { text.classList.remove('blink'); }, 500); } async function refreshTrainLayer() { const getBufferedExtent = (view) => { const extent = view.calculateExtent(); const resolution = view.getResolution(); const buffer = 100 * resolution; const bufferedExtent = [ extent[0] - buffer, extent[1] - buffer, extent[2] + buffer, extent[3] + buffer, ] return bufferedExtent; } return new Promise(async (resolve) => { const bufferedExtent = getBufferedExtent(map.getView()); const bbox = bufferedExtent.toWGS84().join(','); const filter = `${params.trn ? `trnNo=${params.trn}` : `bbox=${bbox}`}`; if (params.trn && params.date) { const date = params.date const now = new Date() const yyyy = now.getFullYear() const mm = (now.getMonth() +1).toString().padStart(2, '0') const dd = now.getDate().toString().padStart(2, '0') const yyyymmdd = `${yyyy}${mm}${dd}`; if (date !== yyyymmdd) { return } } const response = await fetch(`/api/train?${filter}`); const geojson = await response.json(); const newFeatures = ol.source.Vector.fromGeoJSON(geojson).getFeatures(); if (params?.trn) { newFeatures.forEach((f) => { f.set('icon', 'train'); f.set('bearing', 0); }) } else { newFeatures.forEach((f) => { f.set('icon', `${f.get('trn_clsf')}${(f.get('delay') > 20 ? '_delay' : '')}`); }) } const trainSource = map.getLayer('train')?.getSource(); trainSource?.clear(); trainSource?.addFeatures(newFeatures); if (map.get('trainClicked') && map.isShowingOverlay()) { const past = map.get('trainClicked'); const now = newFeatures.find((feature) => (feature.get('trn_no') === past.get('trn_no'))); if (now) { const [pastX, pastY] = past.getGeometry().getCoordinates(); const nowCoords = now.getGeometry().getCoordinates(); const [nowX, nowY] = nowCoords; if ( !(pastX === nowX && pastY === nowY) ) { map.getLayer('train').once('postrender',() => { map.getOverlay().setPosition(nowCoords); map.getView().animate({ center: nowCoords, duration: 500, }); showPopup(now); }) } } else { map.hideOverlay(); } } //sendMessageToApp(); resolve() }) .then(updateRefreshText) } function reloadTrainLayer() { refreshTrainLayer() syncIterator() } async function initTrainLayer() { return new Promise(async (resolve) => { map.addPointsLayer({ name: 'train', style: layerStyles.trainStyle, zIndex: 9, declutter: true, }); map.on('pointerdrag', map.hideOverlay) map.on('moveend', reloadTrainLayer) initRefreshDOM(); resolve() }); } const showPopup = (feature) => { return new Promise(async (resolve, reject) => { map.set('trainClicked', feature); const coordinate = feature.getGeometry().getFlatCoordinates(); const popup = map.getOverlay(); try { const { trn_no, trn_case, trn_clsf, dpt_stn_nm, dpt_pln_dttm, arv_stn_nm, arv_pln_dttm, now_stn, next_stn, delay } = feature.getProperties(); const dpt_time = `${dpt_pln_dttm.slice(8,10)}:${dpt_pln_dttm.slice(10,12)}`; const arv_time = `${arv_pln_dttm.slice(8,10)}:${arv_pln_dttm.slice(10,12)}`; const now_loc = now_stn && next_stn ? `${now_stn}${next_stn}` : ''; html = `